1109 lines
44 KiB
PHP
1109 lines
44 KiB
PHP
<?php
|
|
/**
|
|
* Webhook handler class
|
|
*/
|
|
class TWP_Webhooks {
|
|
|
|
/**
|
|
* Register webhook endpoints
|
|
*/
|
|
public function register_endpoints() {
|
|
// Register REST API endpoints for webhooks
|
|
add_action('rest_api_init', function() {
|
|
// Voice webhook
|
|
register_rest_route('twilio-webhook/v1', '/voice', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_voice_webhook'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// SMS webhook
|
|
register_rest_route('twilio-webhook/v1', '/sms', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_sms_webhook'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Status webhook
|
|
register_rest_route('twilio-webhook/v1', '/status', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_status_webhook'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// IVR response webhook
|
|
register_rest_route('twilio-webhook/v1', '/ivr-response', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_ivr_response'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Queue wait webhook
|
|
register_rest_route('twilio-webhook/v1', '/queue-wait', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_queue_wait'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Voicemail callback webhook
|
|
register_rest_route('twilio-webhook/v1', '/voicemail-callback', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_voicemail_callback'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Transcription webhook
|
|
register_rest_route('twilio-webhook/v1', '/transcription', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_transcription_webhook'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Callback choice webhook
|
|
register_rest_route('twilio-webhook/v1', '/callback-choice', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_callback_choice'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Request callback webhook
|
|
register_rest_route('twilio-webhook/v1', '/request-callback', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_request_callback'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Callback agent webhook
|
|
register_rest_route('twilio-webhook/v1', '/callback-agent', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_callback_agent'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Callback customer webhook
|
|
register_rest_route('twilio-webhook/v1', '/callback-customer', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_callback_customer'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Outbound agent webhook
|
|
register_rest_route('twilio-webhook/v1', '/outbound-agent', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_outbound_agent'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Ring group result webhook
|
|
register_rest_route('twilio-webhook/v1', '/ring-group-result', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_ring_group_result'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Agent connect webhook
|
|
register_rest_route('twilio-webhook/v1', '/agent-connect', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_agent_connect'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
|
|
// Outbound agent with from number webhook
|
|
register_rest_route('twilio-webhook/v1', '/outbound-agent-with-from', array(
|
|
'methods' => 'POST',
|
|
'callback' => array($this, 'handle_outbound_agent_with_from'),
|
|
'permission_callback' => '__return_true'
|
|
));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle webhook requests (deprecated - using REST API now)
|
|
*/
|
|
public function handle_webhook() {
|
|
// This method is deprecated and no longer used
|
|
// Webhooks are now handled via REST API endpoints
|
|
}
|
|
|
|
/**
|
|
* Send TwiML response
|
|
*/
|
|
private function send_twiml_response($twiml) {
|
|
return new WP_REST_Response($twiml, 200, array(
|
|
'Content-Type' => 'text/xml; charset=utf-8'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Verify Twilio signature
|
|
*/
|
|
private function verify_twilio_signature() {
|
|
// Get signature header
|
|
$signature = isset($_SERVER['HTTP_X_TWILIO_SIGNATURE']) ? $_SERVER['HTTP_X_TWILIO_SIGNATURE'] : '';
|
|
|
|
if (!$signature) {
|
|
return false;
|
|
}
|
|
|
|
// Get current URL
|
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
|
$url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
|
|
|
|
// Get POST data
|
|
$postData = file_get_contents('php://input');
|
|
parse_str($postData, $params);
|
|
|
|
// Verify signature
|
|
$twilio = new TWP_Twilio_API();
|
|
return $twilio->validate_webhook_signature($url, $params, $signature);
|
|
}
|
|
|
|
/**
|
|
* Handle voice webhook
|
|
*/
|
|
public function handle_voice_webhook($request) {
|
|
// Verify Twilio signature
|
|
if (!$this->verify_twilio_signature()) {
|
|
return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
|
|
}
|
|
|
|
$params = $request->get_params();
|
|
|
|
$call_data = array(
|
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
|
'From' => isset($params['From']) ? $params['From'] : '',
|
|
'To' => isset($params['To']) ? $params['To'] : '',
|
|
'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : ''
|
|
);
|
|
|
|
// Log the incoming call
|
|
TWP_Call_Logger::log_call(array(
|
|
'call_sid' => $call_data['CallSid'],
|
|
'from_number' => $call_data['From'],
|
|
'to_number' => $call_data['To'],
|
|
'status' => 'initiated',
|
|
'actions_taken' => 'Incoming call received'
|
|
));
|
|
|
|
// Check for schedule
|
|
$schedule_id = isset($params['schedule_id']) ? intval($params['schedule_id']) : 0;
|
|
|
|
if ($schedule_id) {
|
|
$schedule = TWP_Scheduler::get_schedule($schedule_id);
|
|
|
|
if ($schedule && $schedule->workflow_id) {
|
|
// Execute workflow
|
|
TWP_Call_Logger::log_action($call_data['CallSid'], 'Executing workflow: ' . $schedule->schedule_name);
|
|
$twiml = TWP_Workflow::execute_workflow($schedule->workflow_id, $call_data);
|
|
|
|
TWP_Call_Logger::update_call($call_data['CallSid'], array(
|
|
'workflow_id' => $schedule->workflow_id,
|
|
'workflow_name' => $schedule->schedule_name
|
|
));
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
} elseif ($schedule && $schedule->forward_number) {
|
|
// Forward call
|
|
TWP_Call_Logger::log_action($call_data['CallSid'], 'Forwarding to: ' . $schedule->forward_number);
|
|
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addChild('Number', $schedule->forward_number);
|
|
return $this->send_twiml_response($twiml->asXML());
|
|
}
|
|
}
|
|
|
|
// Check for workflow associated with phone number
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
$workflow = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE phone_number = %s AND is_active = 1 LIMIT 1",
|
|
$call_data['To']
|
|
));
|
|
|
|
if ($workflow) {
|
|
TWP_Call_Logger::log_action($call_data['CallSid'], 'Executing workflow: ' . $workflow->workflow_name);
|
|
$twiml = TWP_Workflow::execute_workflow($workflow->id, $call_data);
|
|
|
|
TWP_Call_Logger::update_call($call_data['CallSid'], array(
|
|
'workflow_id' => $workflow->id,
|
|
'workflow_name' => $workflow->workflow_name
|
|
));
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
// Default response
|
|
TWP_Call_Logger::log_action($call_data['CallSid'], 'No workflow found, using default response');
|
|
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$say = $twiml->addChild('Say', 'Thank you for calling. Please hold while we connect you.');
|
|
$say->addAttribute('voice', 'alice');
|
|
|
|
// Add to default queue
|
|
$enqueue = $twiml->addChild('Enqueue', 'default');
|
|
|
|
return $this->send_twiml_response($twiml->asXML());
|
|
}
|
|
|
|
/**
|
|
* Handle SMS webhook
|
|
*/
|
|
private function handle_sms_webhook() {
|
|
$sms_data = array(
|
|
'MessageSid' => isset($_POST['MessageSid']) ? $_POST['MessageSid'] : '',
|
|
'From' => isset($_POST['From']) ? $_POST['From'] : '',
|
|
'To' => isset($_POST['To']) ? $_POST['To'] : '',
|
|
'Body' => isset($_POST['Body']) ? $_POST['Body'] : ''
|
|
);
|
|
|
|
// Process SMS commands
|
|
$command = strtolower(trim($sms_data['Body']));
|
|
|
|
switch ($command) {
|
|
case '1':
|
|
$this->handle_agent_ready_sms($sms_data['From']);
|
|
break;
|
|
|
|
case 'status':
|
|
$this->send_status_sms($sms_data['From']);
|
|
break;
|
|
|
|
case 'help':
|
|
$this->send_help_sms($sms_data['From']);
|
|
break;
|
|
|
|
default:
|
|
// Log SMS for later processing
|
|
$this->log_sms($sms_data);
|
|
break;
|
|
}
|
|
|
|
// Empty response
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
echo $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Handle status webhook
|
|
*/
|
|
public function handle_status_webhook($request) {
|
|
// Verify Twilio signature
|
|
if (!$this->verify_twilio_signature()) {
|
|
return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
|
|
}
|
|
|
|
$params = $request->get_params();
|
|
|
|
$status_data = array(
|
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
|
'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : '',
|
|
'CallDuration' => isset($params['CallDuration']) ? intval($params['CallDuration']) : 0
|
|
);
|
|
|
|
// Update call log with status and duration
|
|
TWP_Call_Logger::update_call($status_data['CallSid'], array(
|
|
'status' => $status_data['CallStatus'],
|
|
'duration' => $status_data['CallDuration'],
|
|
'actions_taken' => 'Call status changed to: ' . $status_data['CallStatus']
|
|
));
|
|
|
|
// Update call status in queue if applicable
|
|
if ($status_data['CallStatus'] === 'completed') {
|
|
TWP_Call_Queue::remove_from_queue($status_data['CallSid']);
|
|
TWP_Call_Logger::log_action($status_data['CallSid'], 'Call removed from queue');
|
|
}
|
|
|
|
// Empty response
|
|
return new WP_REST_Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', 200, array(
|
|
'Content-Type' => 'text/xml; charset=utf-8'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Handle IVR response
|
|
*/
|
|
private function handle_ivr_response() {
|
|
$digits = isset($_POST['Digits']) ? $_POST['Digits'] : '';
|
|
$workflow_id = isset($_GET['workflow_id']) ? intval($_GET['workflow_id']) : 0;
|
|
$step_id = isset($_GET['step_id']) ? intval($_GET['step_id']) : 0;
|
|
|
|
if (!$workflow_id || !$step_id) {
|
|
$this->send_default_response();
|
|
return;
|
|
}
|
|
|
|
$workflow = TWP_Workflow::get_workflow($workflow_id);
|
|
|
|
if (!$workflow) {
|
|
$this->send_default_response();
|
|
return;
|
|
}
|
|
|
|
$workflow_data = json_decode($workflow->workflow_data, true);
|
|
|
|
// Find the step and its options
|
|
foreach ($workflow_data['steps'] as $step) {
|
|
if ($step['id'] == $step_id && isset($step['options'][$digits])) {
|
|
$option = $step['options'][$digits];
|
|
|
|
switch ($option['action']) {
|
|
case 'forward':
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addChild('Number', $option['number']);
|
|
echo $twiml->asXML();
|
|
return;
|
|
|
|
case 'queue':
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$enqueue = $twiml->addChild('Enqueue', $option['queue_name']);
|
|
echo $twiml->asXML();
|
|
return;
|
|
|
|
case 'voicemail':
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
$twiml = TWP_Workflow::create_voicemail_twiml($option, $elevenlabs);
|
|
echo $twiml;
|
|
return;
|
|
|
|
case 'message':
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$say = $twiml->addChild('Say', $option['message']);
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Hangup');
|
|
echo $twiml->asXML();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Invalid option - replay menu
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$say = $twiml->addChild('Say', 'Invalid option. Please try again.');
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Redirect');
|
|
echo $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Handle queue wait
|
|
*/
|
|
private function handle_queue_wait() {
|
|
$queue_id = isset($_GET['queue_id']) ? intval($_GET['queue_id']) : 0;
|
|
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] : '';
|
|
|
|
// Get caller's position in queue
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$call = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE call_sid = %s",
|
|
$call_sid
|
|
));
|
|
|
|
if ($call) {
|
|
$position = $call->position;
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
|
|
// Generate position announcement
|
|
$message = "You are currently number $position in the queue. Your call is important to us.";
|
|
$audio_result = $elevenlabs->text_to_speech($message);
|
|
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
if ($audio_result['success']) {
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
|
|
// Add wait music
|
|
$queue = TWP_Call_Queue::get_queue($queue_id);
|
|
if ($queue && $queue->wait_music_url) {
|
|
$play = $twiml->addChild('Play', $queue->wait_music_url);
|
|
$play->addAttribute('loop', '0');
|
|
}
|
|
|
|
echo $twiml->asXML();
|
|
} else {
|
|
$this->send_default_response();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle voicemail callback
|
|
*/
|
|
public function handle_voicemail_callback($request) {
|
|
// Verify Twilio signature
|
|
if (!$this->verify_twilio_signature()) {
|
|
return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
|
|
}
|
|
|
|
$params = $request->get_params();
|
|
|
|
$recording_url = isset($params['RecordingUrl']) ? $params['RecordingUrl'] : '';
|
|
$recording_duration = isset($params['RecordingDuration']) ? intval($params['RecordingDuration']) : 0;
|
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
|
$from = isset($params['From']) ? $params['From'] : '';
|
|
$workflow_id = isset($params['workflow_id']) ? intval($params['workflow_id']) : 0;
|
|
|
|
if ($recording_url) {
|
|
// Save voicemail record
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_voicemails';
|
|
|
|
$voicemail_id = $wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'workflow_id' => $workflow_id,
|
|
'from_number' => $from,
|
|
'recording_url' => $recording_url,
|
|
'duration' => $recording_duration,
|
|
'created_at' => current_time('mysql')
|
|
),
|
|
array('%d', '%s', '%s', '%d', '%s')
|
|
);
|
|
|
|
// Log voicemail action
|
|
if ($call_sid) {
|
|
TWP_Call_Logger::log_action($call_sid, 'Voicemail recorded (' . $recording_duration . 's)');
|
|
}
|
|
|
|
// Send notification email
|
|
$this->send_voicemail_notification($from, $recording_url, $recording_duration, $voicemail_id);
|
|
|
|
// Schedule transcription if enabled
|
|
$this->schedule_voicemail_transcription($voicemail_id, $recording_url);
|
|
}
|
|
|
|
return new WP_REST_Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', 200, array(
|
|
'Content-Type' => 'text/xml; charset=utf-8'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Send voicemail notification
|
|
*/
|
|
private function send_voicemail_notification($from_number, $recording_url, $duration, $voicemail_id) {
|
|
$admin_email = get_option('admin_email');
|
|
$site_name = get_bloginfo('name');
|
|
|
|
$subject = '[' . $site_name . '] New Voicemail from ' . $from_number;
|
|
|
|
$message = "You have received a new voicemail:\n\n";
|
|
$message .= "From: " . $from_number . "\n";
|
|
$message .= "Duration: " . $duration . " seconds\n";
|
|
$message .= "Received: " . current_time('F j, Y g:i A') . "\n\n";
|
|
$message .= "Listen to the voicemail in your admin panel:\n";
|
|
$message .= admin_url('admin.php?page=twilio-wp-voicemails') . "\n\n";
|
|
$message .= "Direct link to recording:\n";
|
|
$message .= $recording_url;
|
|
|
|
$headers = array('Content-Type: text/plain; charset=UTF-8');
|
|
|
|
wp_mail($admin_email, $subject, $message, $headers);
|
|
|
|
// Also send SMS notification if configured
|
|
$notification_number = get_option('twp_sms_notification_number');
|
|
if ($notification_number) {
|
|
$twilio = new TWP_Twilio_API();
|
|
$sms_message = "New voicemail from {$from_number} ({$duration}s). Check admin panel to listen.";
|
|
$twilio->send_sms($notification_number, $sms_message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule voicemail transcription
|
|
*/
|
|
private function schedule_voicemail_transcription($voicemail_id, $recording_url) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_voicemails';
|
|
|
|
// Mark transcription as pending - Twilio will call our transcription webhook when ready
|
|
$wpdb->update(
|
|
$table_name,
|
|
array('transcription' => 'Transcription pending...'),
|
|
array('id' => $voicemail_id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle transcription webhook
|
|
*/
|
|
public function handle_transcription_webhook($request) {
|
|
// Verify Twilio signature
|
|
if (!$this->verify_twilio_signature()) {
|
|
return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401));
|
|
}
|
|
|
|
$params = $request->get_params();
|
|
|
|
$transcription_text = isset($params['TranscriptionText']) ? $params['TranscriptionText'] : '';
|
|
$recording_sid = isset($params['RecordingSid']) ? $params['RecordingSid'] : '';
|
|
$transcription_status = isset($params['TranscriptionStatus']) ? $params['TranscriptionStatus'] : '';
|
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
|
|
|
if ($transcription_status === 'completed' && $transcription_text) {
|
|
// Find voicemail by recording URL (we need to match by call_sid or recording URL)
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_voicemails';
|
|
|
|
// First try to find by recording URL containing the RecordingSid
|
|
$voicemail = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1",
|
|
'%' . $recording_sid . '%'
|
|
));
|
|
|
|
if ($voicemail) {
|
|
// Update transcription
|
|
$wpdb->update(
|
|
$table_name,
|
|
array('transcription' => $transcription_text),
|
|
array('id' => $voicemail->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
|
|
// Log transcription completion
|
|
if ($call_sid) {
|
|
TWP_Call_Logger::log_action($call_sid, 'Voicemail transcription completed');
|
|
}
|
|
|
|
// Send notification if transcription contains keywords
|
|
$this->check_transcription_keywords($voicemail->id, $transcription_text, $voicemail->from_number);
|
|
}
|
|
} elseif ($transcription_status === 'failed') {
|
|
// Handle failed transcription
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_voicemails';
|
|
|
|
$voicemail = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1",
|
|
'%' . $recording_sid . '%'
|
|
));
|
|
|
|
if ($voicemail) {
|
|
$wpdb->update(
|
|
$table_name,
|
|
array('transcription' => 'Transcription failed'),
|
|
array('id' => $voicemail->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
if ($call_sid) {
|
|
TWP_Call_Logger::log_action($call_sid, 'Voicemail transcription failed');
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', 200, array(
|
|
'Content-Type' => 'text/xml; charset=utf-8'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Check transcription for important keywords
|
|
*/
|
|
private function check_transcription_keywords($voicemail_id, $transcription_text, $from_number) {
|
|
$urgent_keywords = get_option('twp_urgent_keywords', 'urgent,emergency,important,asap,help');
|
|
$keywords = array_map('trim', explode(',', strtolower($urgent_keywords)));
|
|
|
|
$transcription_lower = strtolower($transcription_text);
|
|
|
|
foreach ($keywords as $keyword) {
|
|
if (!empty($keyword) && strpos($transcription_lower, $keyword) !== false) {
|
|
// Send urgent notification
|
|
$this->send_urgent_voicemail_notification($voicemail_id, $transcription_text, $from_number, $keyword);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send urgent voicemail notification
|
|
*/
|
|
private function send_urgent_voicemail_notification($voicemail_id, $transcription_text, $from_number, $keyword) {
|
|
$admin_email = get_option('admin_email');
|
|
$site_name = get_bloginfo('name');
|
|
|
|
$subject = '[URGENT] ' . $site_name . ' - Voicemail from ' . $from_number;
|
|
|
|
$message = "URGENT VOICEMAIL DETECTED:\\n\\n";
|
|
$message .= "From: " . $from_number . "\\n";
|
|
$message .= "Keyword detected: " . $keyword . "\\n";
|
|
$message .= "Received: " . current_time('F j, Y g:i A') . "\\n\\n";
|
|
$message .= "Transcription:\\n" . $transcription_text . "\\n\\n";
|
|
$message .= "Listen to voicemail: " . admin_url('admin.php?page=twilio-wp-voicemails');
|
|
|
|
$headers = array('Content-Type: text/plain; charset=UTF-8');
|
|
|
|
wp_mail($admin_email, $subject, $message, $headers);
|
|
|
|
// Also send urgent SMS notification
|
|
$notification_number = get_option('twp_sms_notification_number');
|
|
if ($notification_number) {
|
|
$twilio = new TWP_Twilio_API();
|
|
$sms_message = "URGENT voicemail from {$from_number}. Keyword: {$keyword}. Check admin panel immediately.";
|
|
$twilio->send_sms($notification_number, $sms_message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send default response
|
|
*/
|
|
private function send_default_response() {
|
|
$response = new \Twilio\TwiML\VoiceResponse();
|
|
$response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']);
|
|
$response->hangup();
|
|
echo $response->asXML();
|
|
}
|
|
|
|
/**
|
|
* Send status SMS
|
|
*/
|
|
private function send_status_sms($to_number) {
|
|
$twilio = new TWP_Twilio_API();
|
|
$queue_status = TWP_Call_Queue::get_queue_status();
|
|
|
|
$message = "Queue Status:\n";
|
|
foreach ($queue_status as $queue) {
|
|
$message .= $queue['queue_name'] . ': ' . $queue['waiting_calls'] . " waiting\n";
|
|
}
|
|
|
|
$twilio->send_sms($to_number, $message);
|
|
}
|
|
|
|
/**
|
|
* Send help SMS
|
|
*/
|
|
private function send_help_sms($to_number) {
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
$message = "Available commands:\n";
|
|
$message .= "STATUS - Get queue status\n";
|
|
$message .= "HELP - Show this message";
|
|
|
|
$twilio->send_sms($to_number, $message);
|
|
}
|
|
|
|
/**
|
|
* Log SMS
|
|
*/
|
|
private function log_sms($sms_data) {
|
|
// Store SMS in database for later processing
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_sms_log';
|
|
|
|
$wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'message_sid' => $sms_data['MessageSid'],
|
|
'from_number' => $sms_data['From'],
|
|
'to_number' => $sms_data['To'],
|
|
'body' => $sms_data['Body'],
|
|
'received_at' => current_time('mysql')
|
|
),
|
|
array('%s', '%s', '%s', '%s', '%s')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Log call status
|
|
*/
|
|
private function log_call_status($status_data) {
|
|
// Store call status in database
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_call_log';
|
|
|
|
$wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'call_sid' => $status_data['CallSid'],
|
|
'status' => $status_data['CallStatus'],
|
|
'duration' => $status_data['CallDuration'],
|
|
'updated_at' => current_time('mysql')
|
|
),
|
|
array('%s', '%s', '%d', '%s')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle callback choice webhook
|
|
*/
|
|
public function handle_callback_choice($request) {
|
|
$params = $request->get_params();
|
|
$digits = isset($params['Digits']) ? $params['Digits'] : '';
|
|
$phone_number = isset($params['Caller']) ? $params['Caller'] : '';
|
|
$queue_id = isset($params['queue_id']) ? intval($params['queue_id']) : null;
|
|
|
|
if ($digits === '1') {
|
|
// User chose to wait - redirect back to queue
|
|
$twiml = '<Response><Say voice="alice">Returning you to the queue.</Say><Redirect>' . home_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id) . '</Redirect></Response>';
|
|
} else {
|
|
// Default to callback (digits === '2' or no input)
|
|
TWP_Callback_Manager::request_callback($phone_number, $queue_id);
|
|
$twiml = '<Response><Say voice="alice">Your callback has been requested. We will call you back shortly. Thank you!</Say><Hangup/></Response>';
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Handle request callback webhook
|
|
*/
|
|
public function handle_request_callback($request) {
|
|
$params = $request->get_params();
|
|
$phone_number = isset($params['phone_number']) ? $params['phone_number'] : '';
|
|
$queue_id = isset($params['queue_id']) ? intval($params['queue_id']) : null;
|
|
|
|
if ($phone_number) {
|
|
TWP_Callback_Manager::request_callback($phone_number, $queue_id);
|
|
$twiml = '<Response><Say voice="alice">Your callback has been requested. We will call you back shortly. Thank you!</Say><Hangup/></Response>';
|
|
} else {
|
|
$twiml = '<Response><Say voice="alice">Unable to process your callback request. Please try again.</Say><Hangup/></Response>';
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Handle callback agent webhook
|
|
*/
|
|
public function handle_callback_agent($request) {
|
|
$params = $request->get_params();
|
|
$callback_id = isset($params['callback_id']) ? intval($params['callback_id']) : 0;
|
|
$customer_number = isset($params['customer_number']) ? $params['customer_number'] : '';
|
|
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
|
|
|
if ($callback_id && $customer_number) {
|
|
TWP_Callback_Manager::handle_agent_answered($callback_id, $call_sid);
|
|
$twiml = '<Response><Say voice="alice">Please hold while we connect the customer.</Say><Play>http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.wav</Play></Response>';
|
|
} else {
|
|
$twiml = '<Response><Say voice="alice">Unable to process callback. Hanging up.</Say><Hangup/></Response>';
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Handle callback customer webhook
|
|
*/
|
|
public function handle_callback_customer($request) {
|
|
$params = $request->get_params();
|
|
$agent_call_sid = isset($params['agent_call_sid']) ? $params['agent_call_sid'] : '';
|
|
$callback_id = isset($params['callback_id']) ? intval($params['callback_id']) : 0;
|
|
|
|
if ($agent_call_sid) {
|
|
// Conference both calls together
|
|
$conference_name = 'callback-' . $callback_id . '-' . time();
|
|
$twiml = '<Response><Say voice="alice">You are being connected to an agent.</Say><Dial><Conference>' . $conference_name . '</Conference></Dial></Response>';
|
|
|
|
// Update the agent call to join the same conference
|
|
$twilio = new TWP_Twilio_API();
|
|
$agent_twiml = '<Response><Dial><Conference>' . $conference_name . '</Conference></Dial></Response>';
|
|
$twilio->update_call($agent_call_sid, array('Twiml' => $agent_twiml));
|
|
|
|
// Mark callback as completed
|
|
TWP_Callback_Manager::complete_callback($callback_id);
|
|
} else {
|
|
$twiml = '<Response><Say voice="alice">Unable to connect your call. Please try again later.</Say><Hangup/></Response>';
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Handle outbound agent webhook
|
|
*/
|
|
public function handle_outbound_agent($request) {
|
|
$params = $request->get_params();
|
|
$target_number = isset($params['target_number']) ? $params['target_number'] : '';
|
|
$agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
|
|
|
if ($target_number) {
|
|
$twiml = TWP_Callback_Manager::handle_outbound_agent_answered($target_number, $agent_call_sid);
|
|
} else {
|
|
$twiml = '<Response><Say voice="alice">Unable to process outbound call.</Say><Hangup/></Response>';
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Handle ring group result webhook
|
|
*/
|
|
public function handle_ring_group_result($request) {
|
|
$params = $request->get_params();
|
|
$dial_call_status = isset($params['DialCallStatus']) ? $params['DialCallStatus'] : '';
|
|
$group_id = isset($params['group_id']) ? intval($params['group_id']) : 0;
|
|
$queue_name = isset($params['queue_name']) ? $params['queue_name'] : '';
|
|
$fallback_action = isset($params['fallback_action']) ? $params['fallback_action'] : 'queue';
|
|
$caller_number = isset($params['From']) ? $params['From'] : '';
|
|
|
|
// If the call was answered, just hang up (call is connected)
|
|
if ($dial_call_status === 'completed') {
|
|
$twiml = '<Response></Response>';
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
// If no one answered, handle based on fallback action
|
|
if ($dial_call_status === 'no-answer' || $dial_call_status === 'busy' || $dial_call_status === 'failed') {
|
|
|
|
if ($fallback_action === 'queue' && !empty($queue_name)) {
|
|
// Put caller back in queue
|
|
$twiml = '<Response>';
|
|
$twiml .= '<Say voice="alice">No agents are currently available. Adding you to the queue.</Say>';
|
|
$twiml .= '<Enqueue waitUrl="' . home_url('/wp-json/twilio-webhook/v1/queue-wait?queue_name=' . urlencode($queue_name)) . '">' . $queue_name . '</Enqueue>';
|
|
$twiml .= '</Response>';
|
|
|
|
// Notify group members via SMS if no agents are available
|
|
$this->notify_group_members_sms($group_id, $caller_number, $queue_name);
|
|
|
|
} else if ($fallback_action === 'voicemail') {
|
|
// Go to voicemail
|
|
$twiml = '<Response>';
|
|
$twiml .= '<Say voice="alice">No one is available to take your call. Please leave a message after the beep.</Say>';
|
|
$twiml .= '<Record action="' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . '" maxLength="300" playBeep="true" finishOnKey="#"/>';
|
|
$twiml .= '</Response>';
|
|
|
|
} else {
|
|
// Default message and callback option
|
|
$callback_twiml = TWP_Callback_Manager::create_callback_twiml(null, $caller_number);
|
|
return $this->send_twiml_response($callback_twiml);
|
|
}
|
|
} else {
|
|
// Unknown status, provide callback option
|
|
$callback_twiml = TWP_Callback_Manager::create_callback_twiml(null, $caller_number);
|
|
return $this->send_twiml_response($callback_twiml);
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Notify group members via SMS when no agents are available
|
|
*/
|
|
private function notify_group_members_sms($group_id, $caller_number, $queue_name) {
|
|
$members = TWP_Agent_Groups::get_group_members($group_id);
|
|
$twilio = new TWP_Twilio_API();
|
|
$sms_number = get_option('twp_sms_notification_number');
|
|
|
|
if (empty($sms_number) || empty($members)) {
|
|
return;
|
|
}
|
|
|
|
$message = "Call waiting in queue '{$queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call.";
|
|
|
|
foreach ($members as $member) {
|
|
$agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true);
|
|
|
|
if (!empty($agent_phone)) {
|
|
// Send SMS notification
|
|
$twilio->send_sms($agent_phone, $message);
|
|
|
|
// Log the notification
|
|
error_log("TWP: SMS notification sent to agent {$member->user_id} at {$agent_phone}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle agent ready SMS (when agent texts "1")
|
|
*/
|
|
private function handle_agent_ready_sms($agent_phone) {
|
|
// Find user by phone number
|
|
$users = get_users(array(
|
|
'meta_key' => 'twp_phone_number',
|
|
'meta_value' => $agent_phone,
|
|
'meta_compare' => '='
|
|
));
|
|
|
|
if (empty($users)) {
|
|
// Send error message if agent not found
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->send_sms($agent_phone, "Phone number not found in system. Please contact administrator.");
|
|
return;
|
|
}
|
|
|
|
$user = $users[0];
|
|
$user_id = $user->ID;
|
|
|
|
// Set agent status to available
|
|
TWP_Agent_Manager::set_agent_status($user_id, 'available');
|
|
|
|
// Check for waiting calls and assign one if available
|
|
$assigned_call = $this->try_assign_call_to_agent($user_id, $agent_phone);
|
|
|
|
if ($assigned_call) {
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->send_sms($agent_phone, "Call assigned! You should receive the call shortly.");
|
|
} else {
|
|
// No waiting calls, just confirm availability
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->send_sms($agent_phone, "Status updated to available. You'll receive the next waiting call.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle agent connect webhook (when agent answers SMS-triggered call)
|
|
*/
|
|
public function handle_agent_connect($request) {
|
|
$params = $request->get_params();
|
|
$queued_call_id = isset($params['queued_call_id']) ? intval($params['queued_call_id']) : 0;
|
|
$customer_number = isset($params['customer_number']) ? $params['customer_number'] : '';
|
|
$agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
|
|
|
|
if (!$queued_call_id || !$customer_number) {
|
|
$twiml = '<Response><Say voice="alice">Unable to connect call.</Say><Hangup/></Response>';
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
// Create conference to connect agent and customer
|
|
$conference_name = 'queue-connect-' . $queued_call_id . '-' . time();
|
|
|
|
$twiml = '<Response>';
|
|
$twiml .= '<Say voice="alice">Connecting you to the customer now.</Say>';
|
|
$twiml .= '<Dial><Conference>' . $conference_name . '</Conference></Dial>';
|
|
$twiml .= '</Response>';
|
|
|
|
// Get the customer's call and redirect to conference
|
|
global $wpdb;
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
$queued_call = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $calls_table WHERE id = %d",
|
|
$queued_call_id
|
|
));
|
|
|
|
if ($queued_call) {
|
|
// Connect customer to the same conference
|
|
$customer_twiml = '<Response>';
|
|
$customer_twiml .= '<Say voice="alice">An agent is now available. Connecting you now.</Say>';
|
|
$customer_twiml .= '<Dial><Conference>' . $conference_name . '</Conference></Dial>';
|
|
$customer_twiml .= '</Response>';
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
$twilio->update_call($queued_call->call_sid, array('Twiml' => $customer_twiml));
|
|
|
|
// Update call status to connected
|
|
$wpdb->update(
|
|
$calls_table,
|
|
array('status' => 'connected'),
|
|
array('id' => $queued_call_id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
return $this->send_twiml_response($twiml);
|
|
}
|
|
|
|
/**
|
|
* Try to assign a waiting call to the agent
|
|
*/
|
|
private function try_assign_call_to_agent($user_id, $agent_phone) {
|
|
global $wpdb;
|
|
$calls_table = $wpdb->prefix . 'twp_queued_calls';
|
|
|
|
// Find the longest waiting call
|
|
$waiting_call = $wpdb->get_row("
|
|
SELECT * FROM $calls_table
|
|
WHERE status = 'waiting'
|
|
ORDER BY joined_at ASC
|
|
LIMIT 1
|
|
");
|
|
|
|
if (!$waiting_call) {
|
|
return false;
|
|
}
|
|
|
|
// Make call to agent
|
|
$twilio = new TWP_Twilio_API();
|
|
$call_result = $twilio->make_call(
|
|
$agent_phone,
|
|
home_url('/wp-json/twilio-webhook/v1/agent-connect'),
|
|
array(
|
|
'queued_call_id' => $waiting_call->id,
|
|
'customer_number' => $waiting_call->from_number
|
|
)
|
|
);
|
|
|
|
if ($call_result['success']) {
|
|
// Update queued call status
|
|
$wpdb->update(
|
|
$calls_table,
|
|
array(
|
|
'status' => 'connecting',
|
|
'answered_at' => current_time('mysql')
|
|
),
|
|
array('id' => $waiting_call->id),
|
|
array('%s', '%s'),
|
|
array('%d')
|
|
);
|
|
|
|
// Set agent to busy
|
|
TWP_Agent_Manager::set_agent_status($user_id, 'busy', $call_result['call_sid']);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle outbound agent with from number webhook
|
|
*/
|
|
public function handle_outbound_agent_with_from($request) {
|
|
try {
|
|
$params = $request->get_params();
|
|
|
|
// Get parameters from query string or POST body
|
|
$target_number = $request->get_param('target_number') ?: '';
|
|
$from_number = $request->get_param('from_number') ?: '';
|
|
$agent_call_sid = $request->get_param('CallSid') ?: '';
|
|
|
|
// Log parameters for debugging
|
|
error_log('TWP Outbound Webhook - Target: ' . $target_number . ', From: ' . $from_number . ', CallSid: ' . $agent_call_sid);
|
|
error_log('TWP Outbound Webhook - All params: ' . print_r($params, true));
|
|
|
|
if ($target_number && $from_number) {
|
|
// Create TwiML using SDK directly
|
|
$response = new \Twilio\TwiML\VoiceResponse();
|
|
$response->say('Connecting your outbound call...', ['voice' => 'alice']);
|
|
$response->dial($target_number, ['callerId' => $from_number, 'timeout' => 30]);
|
|
|
|
// If call isn't answered, the TwiML will handle the fallback
|
|
return $this->send_twiml_response($response->asXML());
|
|
} else {
|
|
// Enhanced error message with debugging info
|
|
$error_msg = 'Unable to process outbound call.';
|
|
if (empty($target_number)) {
|
|
$error_msg .= ' Missing target number.';
|
|
}
|
|
if (empty($from_number)) {
|
|
$error_msg .= ' Missing from number.';
|
|
}
|
|
|
|
error_log('TWP Outbound Error: ' . $error_msg . ' Params: ' . json_encode($params));
|
|
|
|
$error_response = new \Twilio\TwiML\VoiceResponse();
|
|
$error_response->say($error_msg, ['voice' => 'alice']);
|
|
$error_response->hangup();
|
|
return $this->send_twiml_response($error_response->asXML());
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TWP Outbound Webhook Exception: ' . $e->getMessage());
|
|
$exception_response = new \Twilio\TwiML\VoiceResponse();
|
|
$exception_response->say('Technical error occurred. Please try again.', ['voice' => 'alice']);
|
|
$exception_response->hangup();
|
|
return $this->send_twiml_response($exception_response->asXML());
|
|
}
|
|
}
|
|
} |