1040 lines
44 KiB
PHP
1040 lines
44 KiB
PHP
<?php
|
|
/**
|
|
* Workflow management class
|
|
*/
|
|
class TWP_Workflow {
|
|
|
|
/**
|
|
* Create workflow
|
|
*/
|
|
public static function create_workflow($data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
$workflow_data = array(
|
|
'steps' => $data['steps'],
|
|
'conditions' => $data['conditions'],
|
|
'actions' => $data['actions']
|
|
);
|
|
|
|
return $wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'workflow_name' => sanitize_text_field($data['workflow_name']),
|
|
'phone_number' => sanitize_text_field($data['phone_number']),
|
|
'workflow_data' => json_encode($workflow_data),
|
|
'is_active' => isset($data['is_active']) ? 1 : 0
|
|
),
|
|
array('%s', '%s', '%s', '%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Execute workflow
|
|
*/
|
|
public static function execute_workflow($workflow_id, $call_data) {
|
|
$workflow = self::get_workflow($workflow_id);
|
|
|
|
if (!$workflow || !$workflow->is_active) {
|
|
return false;
|
|
}
|
|
|
|
$workflow_data = json_decode($workflow->workflow_data, true);
|
|
$twilio = new TWP_Twilio_API();
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
|
|
// Store call data globally for access in step functions
|
|
$GLOBALS['call_data'] = $call_data;
|
|
|
|
// Initialize combined TwiML response
|
|
$response = new \Twilio\TwiML\VoiceResponse();
|
|
$has_response = false;
|
|
|
|
error_log('TWP Workflow: Starting execution for workflow ID: ' . $workflow_id);
|
|
error_log('TWP Workflow: Call data: ' . json_encode($call_data));
|
|
error_log('TWP Workflow: Steps count: ' . count($workflow_data['steps']));
|
|
|
|
// Process workflow steps
|
|
foreach ($workflow_data['steps'] as $step) {
|
|
// Check conditions first
|
|
if (isset($step['conditions'])) {
|
|
if (!self::check_conditions($step['conditions'], $call_data)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$step_twiml = null;
|
|
$stop_after_step = false;
|
|
|
|
switch ($step['type']) {
|
|
case 'greeting':
|
|
$step_twiml = self::create_greeting_twiml($step, $elevenlabs);
|
|
break;
|
|
|
|
case 'ivr_menu':
|
|
$step_twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
|
$stop_after_step = true; // IVR menu needs user input, stop here
|
|
break;
|
|
|
|
case 'forward':
|
|
$step_twiml = self::create_forward_twiml($step);
|
|
$stop_after_step = true; // Forward ends the workflow
|
|
break;
|
|
|
|
case 'queue':
|
|
error_log('TWP Workflow: Processing queue step: ' . json_encode($step));
|
|
$step_twiml = self::create_queue_twiml($step, $elevenlabs);
|
|
$stop_after_step = true; // Queue ends the workflow
|
|
break;
|
|
|
|
case 'ring_group':
|
|
$step_twiml = self::create_ring_group_twiml($step);
|
|
$stop_after_step = true; // Ring group ends the workflow
|
|
break;
|
|
|
|
case 'voicemail':
|
|
// Add workflow_id to the step data
|
|
$step['workflow_id'] = $workflow_id;
|
|
$step_twiml = self::create_voicemail_twiml($step, $elevenlabs);
|
|
$stop_after_step = true; // Voicemail recording ends the workflow
|
|
break;
|
|
|
|
case 'schedule_check':
|
|
$schedule_result = self::handle_schedule_check($step, $call_data);
|
|
if ($schedule_result === false) {
|
|
// Continue to next step (within business hours)
|
|
error_log('TWP Schedule Check: Within business hours, continuing to next step');
|
|
continue 2;
|
|
} elseif ($schedule_result) {
|
|
// After-hours steps returned TwiML - execute and stop
|
|
error_log('TWP Schedule Check: After hours, executing after-hours steps');
|
|
$step_twiml = $schedule_result;
|
|
$stop_after_step = true;
|
|
} else {
|
|
// No schedule or no after-hours steps - continue with next step
|
|
error_log('TWP Schedule Check: No schedule configured or no after-hours steps, continuing');
|
|
continue 2;
|
|
}
|
|
break;
|
|
|
|
case 'sms':
|
|
self::send_sms_notification($step, $call_data);
|
|
continue 2;
|
|
|
|
default:
|
|
continue 2;
|
|
}
|
|
|
|
// Add step TwiML to combined response
|
|
if ($step_twiml) {
|
|
// Parse the step TwiML and append to combined response
|
|
$step_xml = simplexml_load_string($step_twiml);
|
|
if ($step_xml) {
|
|
foreach ($step_xml->children() as $element) {
|
|
self::append_twiml_element($response, $element);
|
|
}
|
|
$has_response = true;
|
|
}
|
|
|
|
// Stop processing if this step type should end the workflow
|
|
if ($stop_after_step) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return combined response or default
|
|
if ($has_response) {
|
|
return $response->asXML();
|
|
}
|
|
|
|
// Default response
|
|
return self::create_default_response();
|
|
}
|
|
|
|
/**
|
|
* Helper function to append SimpleXMLElement to TwiML Response
|
|
*/
|
|
private static function append_twiml_element($response, $element) {
|
|
$name = $element->getName();
|
|
$text = (string) $element;
|
|
$attributes = array();
|
|
|
|
foreach ($element->attributes() as $key => $value) {
|
|
$attributes[$key] = (string) $value;
|
|
}
|
|
|
|
// Handle different TwiML verbs
|
|
switch ($name) {
|
|
case 'Say':
|
|
$response->say($text, $attributes);
|
|
break;
|
|
case 'Play':
|
|
$response->play($text, $attributes);
|
|
break;
|
|
case 'Gather':
|
|
$gather = $response->gather($attributes);
|
|
// Add child elements to gather
|
|
foreach ($element->children() as $child) {
|
|
$child_name = $child->getName();
|
|
if ($child_name === 'Say') {
|
|
$gather->say((string) $child, self::get_attributes($child));
|
|
} elseif ($child_name === 'Play') {
|
|
$gather->play((string) $child, self::get_attributes($child));
|
|
}
|
|
}
|
|
break;
|
|
case 'Record':
|
|
$response->record($attributes);
|
|
break;
|
|
case 'Dial':
|
|
$response->dial((string) $element, $attributes);
|
|
break;
|
|
case 'Queue':
|
|
$response->queue((string) $element, $attributes);
|
|
break;
|
|
case 'Redirect':
|
|
$response->redirect((string) $element, $attributes);
|
|
break;
|
|
case 'Pause':
|
|
$response->pause($attributes);
|
|
break;
|
|
case 'Hangup':
|
|
$response->hangup();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to get attributes as array
|
|
*/
|
|
private static function get_attributes($element) {
|
|
$attributes = array();
|
|
foreach ($element->attributes() as $key => $value) {
|
|
$attributes[$key] = (string) $value;
|
|
}
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Create greeting TwiML
|
|
*/
|
|
private static function create_greeting_twiml($step, $elevenlabs) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// Get message from either data array or direct property
|
|
$message = null;
|
|
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
|
$message = $step['data']['message'];
|
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
|
$message = $step['message'];
|
|
} else {
|
|
$message = 'Welcome to our phone system.';
|
|
}
|
|
|
|
// Check for new audio_type structure or legacy use_tts
|
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
|
|
|
switch ($audio_type) {
|
|
case 'tts':
|
|
// Generate TTS audio
|
|
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
|
(isset($step['voice_id']) ? $step['voice_id'] : null);
|
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
|
'voice_id' => $voice_id
|
|
]);
|
|
|
|
if ($audio_result['success']) {
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
// Fallback to Say
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'audio':
|
|
// Use provided audio file
|
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
|
|
|
if ($audio_url && !empty($audio_url)) {
|
|
$play = $twiml->addChild('Play', $audio_url);
|
|
} else {
|
|
// Fallback to Say if no audio URL provided
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
default: // 'say' or fallback
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
break;
|
|
}
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Create IVR menu TwiML
|
|
*/
|
|
private static function create_ivr_menu_twiml($step, $elevenlabs) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
$gather = $twiml->addChild('Gather');
|
|
$gather->addAttribute('numDigits', isset($step['data']['num_digits']) ? $step['data']['num_digits'] :
|
|
(isset($step['num_digits']) ? $step['num_digits'] : '1'));
|
|
$gather->addAttribute('timeout', isset($step['data']['timeout']) ? $step['data']['timeout'] :
|
|
(isset($step['timeout']) ? $step['timeout'] : '10'));
|
|
|
|
if (isset($step['action_url'])) {
|
|
$gather->addAttribute('action', $step['action_url']);
|
|
} else {
|
|
$webhook_url = home_url('/wp-json/twilio-webhook/v1/ivr-response');
|
|
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
|
|
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
|
|
$gather->addAttribute('action', $webhook_url);
|
|
}
|
|
|
|
// Get message from either data array or direct property
|
|
$message = null;
|
|
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
|
$message = $step['data']['message'];
|
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
|
$message = $step['message'];
|
|
} else {
|
|
$message = 'Please select an option.';
|
|
}
|
|
|
|
// Check for new audio_type structure or legacy use_tts
|
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
|
|
|
switch ($audio_type) {
|
|
case 'tts':
|
|
// Generate TTS audio
|
|
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
|
(isset($step['voice_id']) ? $step['voice_id'] : null);
|
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
|
'voice_id' => $voice_id
|
|
]);
|
|
|
|
if ($audio_result['success']) {
|
|
$play = $gather->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
// Fallback to Say
|
|
$say = $gather->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'audio':
|
|
// Use provided audio file
|
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
|
|
|
if ($audio_url && !empty($audio_url)) {
|
|
$play = $gather->addChild('Play', $audio_url);
|
|
} else {
|
|
// Fallback to Say if no audio URL provided
|
|
$say = $gather->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
default: // 'say' or fallback
|
|
$say = $gather->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
break;
|
|
}
|
|
|
|
// Fallback if no input
|
|
if (isset($step['no_input_action'])) {
|
|
switch ($step['no_input_action']) {
|
|
case 'repeat':
|
|
$redirect = $twiml->addChild('Redirect');
|
|
break;
|
|
|
|
case 'hangup':
|
|
$say = $twiml->addChild('Say', 'Goodbye');
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Hangup');
|
|
break;
|
|
|
|
case 'forward':
|
|
if (isset($step['forward_number'])) {
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('answerOnBridge', 'true');
|
|
$dial->addChild('Number', $step['forward_number']);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Create forward TwiML
|
|
*/
|
|
private static function create_forward_twiml($step) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('answerOnBridge', 'true');
|
|
|
|
if (isset($step['timeout'])) {
|
|
$dial->addAttribute('timeout', $step['timeout']);
|
|
}
|
|
|
|
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
|
|
// Sequential forwarding
|
|
foreach ($step['forward_numbers'] as $number) {
|
|
$dial->addChild('Number', $number);
|
|
}
|
|
} elseif (isset($step['forward_number'])) {
|
|
$dial->addChild('Number', $step['forward_number']);
|
|
}
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Create queue TwiML
|
|
*/
|
|
private static function create_queue_twiml($step, $elevenlabs) {
|
|
error_log('TWP Workflow: Creating queue TwiML with step data: ' . print_r($step, true));
|
|
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// Check if data is nested (workflow steps have data nested)
|
|
$step_data = isset($step['data']) ? $step['data'] : $step;
|
|
|
|
// Get the actual queue name from the database if we have a queue_id
|
|
$queue_name = '';
|
|
$queue_id = null;
|
|
|
|
if (isset($step_data['queue_id']) && !empty($step_data['queue_id'])) {
|
|
$queue_id = $step_data['queue_id'];
|
|
error_log('TWP Workflow: Looking up queue with ID: ' . $queue_id);
|
|
$queue = TWP_Call_Queue::get_queue($queue_id);
|
|
if ($queue) {
|
|
$queue_name = $queue->queue_name;
|
|
error_log('TWP Workflow: Found queue name: ' . $queue_name);
|
|
} else {
|
|
error_log('TWP Workflow: Queue not found in database for ID: ' . $queue_id);
|
|
}
|
|
} elseif (isset($step_data['queue_name']) && !empty($step_data['queue_name'])) {
|
|
// Fallback to queue_name if provided directly
|
|
$queue_name = $step_data['queue_name'];
|
|
error_log('TWP Workflow: Using queue_name directly: ' . $queue_name);
|
|
} else {
|
|
error_log('TWP Workflow: No queue_id or queue_name in step data');
|
|
}
|
|
|
|
// Log error if no queue name found
|
|
if (empty($queue_name)) {
|
|
error_log('TWP Workflow: ERROR - Queue name is empty after lookup');
|
|
// Return error message instead of empty queue
|
|
$say = $twiml->addChild('Say', 'Sorry, the queue is not configured properly. Please try again later.');
|
|
$say->addAttribute('voice', 'alice');
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
error_log('TWP Workflow: Using queue name for Enqueue: ' . $queue_name);
|
|
|
|
// Add call to queue database BEFORE generating Enqueue TwiML
|
|
// Get call info from current request context or call_data parameter
|
|
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
|
|
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
|
|
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : ''));
|
|
$from_number = isset($_POST['From']) ? $_POST['From'] :
|
|
(isset($_REQUEST['From']) ? $_REQUEST['From'] :
|
|
(isset($GLOBALS['call_data']['From']) ? $GLOBALS['call_data']['From'] : ''));
|
|
$to_number = isset($_POST['To']) ? $_POST['To'] :
|
|
(isset($_REQUEST['To']) ? $_REQUEST['To'] :
|
|
(isset($GLOBALS['call_data']['To']) ? $GLOBALS['call_data']['To'] : ''));
|
|
|
|
error_log('TWP Queue: Call data - SID: ' . $call_sid . ', From: ' . $from_number . ', To: ' . $to_number);
|
|
|
|
if ($call_sid && $queue_id) {
|
|
error_log('TWP Workflow: Adding call to queue database - CallSid: ' . $call_sid . ', Queue ID: ' . $queue_id);
|
|
$add_result = TWP_Call_Queue::add_to_queue($queue_id, array(
|
|
'call_sid' => $call_sid,
|
|
'from_number' => $from_number,
|
|
'to_number' => $to_number
|
|
));
|
|
error_log('TWP Workflow: Add to queue result: ' . ($add_result ? 'success' : 'failed'));
|
|
} else {
|
|
error_log('TWP Workflow: Cannot add to queue - missing CallSid (' . $call_sid . ') or queue_id (' . $queue_id . ')');
|
|
}
|
|
|
|
// Instead of using Twilio's Enqueue, redirect to our queue wait handler
|
|
// This gives us complete control over the queue experience
|
|
|
|
// Get announcement message
|
|
$message = '';
|
|
if (isset($step_data['announce_message']) && !empty($step_data['announce_message'])) {
|
|
$message = $step_data['announce_message'];
|
|
} else {
|
|
$message = 'Please hold while we connect you to the next available agent.';
|
|
}
|
|
|
|
// Handle audio type for queue announcement (same logic as other steps)
|
|
$audio_type = isset($step_data['audio_type']) ? $step_data['audio_type'] : 'say';
|
|
|
|
switch ($audio_type) {
|
|
case 'tts':
|
|
// Generate TTS audio
|
|
$voice_id = isset($step_data['voice_id']) ? $step_data['voice_id'] : null;
|
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
|
'voice_id' => $voice_id
|
|
]);
|
|
|
|
if ($audio_result['success']) {
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
// Fallback to Say
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'audio':
|
|
// Use provided audio file
|
|
$audio_url = isset($step_data['audio_url']) ? $step_data['audio_url'] : null;
|
|
|
|
if ($audio_url && !empty($audio_url)) {
|
|
$play = $twiml->addChild('Play', $audio_url);
|
|
} else {
|
|
// Fallback to Say if no audio URL provided
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
default: // 'say'
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
break;
|
|
}
|
|
|
|
// Build the redirect URL properly
|
|
$wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
|
|
$wait_url = add_query_arg(array(
|
|
'queue_id' => $queue_id,
|
|
'call_sid' => urlencode($call_sid) // URL encode to handle special characters
|
|
), $wait_url);
|
|
|
|
// Set the text content of Redirect element properly
|
|
$redirect = $twiml->addChild('Redirect');
|
|
$redirect[0] = $wait_url; // Set the URL as the text content
|
|
$redirect->addAttribute('method', 'POST');
|
|
|
|
error_log('TWP Workflow: Redirecting to custom queue wait handler: ' . $wait_url);
|
|
|
|
$result = $twiml->asXML();
|
|
error_log('TWP Workflow: Final Queue TwiML: ' . $result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Create ring group TwiML
|
|
*/
|
|
private static function create_ring_group_twiml($step) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
if (isset($step['announce_message'])) {
|
|
$say = $twiml->addChild('Say', $step['announce_message']);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
|
|
// Get group phone numbers
|
|
$group_id = intval($step['group_id']);
|
|
$phone_numbers = TWP_Agent_Groups::get_group_phone_numbers($group_id);
|
|
|
|
if (empty($phone_numbers)) {
|
|
$say = $twiml->addChild('Say', 'No agents are available in this group. Please try again later.');
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Hangup');
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('answerOnBridge', 'true');
|
|
|
|
if (isset($step['timeout'])) {
|
|
$dial->addAttribute('timeout', $step['timeout']);
|
|
} else {
|
|
$dial->addAttribute('timeout', '30');
|
|
}
|
|
|
|
if (isset($step['caller_id'])) {
|
|
$dial->addAttribute('callerId', $step['caller_id']);
|
|
}
|
|
|
|
// Set action URL to handle no-answer scenarios
|
|
$action_url = home_url('/wp-json/twilio-webhook/v1/ring-group-result?' . http_build_query([
|
|
'group_id' => $group_id,
|
|
'queue_name' => isset($step['queue_name']) ? $step['queue_name'] : null,
|
|
'fallback_action' => isset($step['fallback_action']) ? $step['fallback_action'] : 'queue'
|
|
]));
|
|
$dial->addAttribute('action', $action_url);
|
|
|
|
// Add all group numbers for simultaneous ring
|
|
foreach ($phone_numbers as $number) {
|
|
if (!empty($number)) {
|
|
$dial->addChild('Number', $number);
|
|
}
|
|
}
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Create voicemail TwiML
|
|
*/
|
|
private static function create_voicemail_twiml($step, $elevenlabs) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// Debug logging
|
|
error_log('TWP Voicemail Step Data: ' . json_encode($step));
|
|
|
|
// Check for greeting message in different possible field names
|
|
// The step data might be nested in a 'data' object
|
|
$greeting = null;
|
|
if (isset($step['data']['greeting_message']) && !empty($step['data']['greeting_message'])) {
|
|
$greeting = $step['data']['greeting_message'];
|
|
error_log('TWP Voicemail: Using data.greeting_message: ' . $greeting);
|
|
} elseif (isset($step['greeting_message']) && !empty($step['greeting_message'])) {
|
|
$greeting = $step['greeting_message'];
|
|
error_log('TWP Voicemail: Using greeting_message: ' . $greeting);
|
|
} elseif (isset($step['data']['message']) && !empty($step['data']['message'])) {
|
|
$greeting = $step['data']['message'];
|
|
error_log('TWP Voicemail: Using data.message: ' . $greeting);
|
|
} elseif (isset($step['message']) && !empty($step['message'])) {
|
|
$greeting = $step['message'];
|
|
error_log('TWP Voicemail: Using message: ' . $greeting);
|
|
} elseif (isset($step['data']['prompt']) && !empty($step['data']['prompt'])) {
|
|
$greeting = $step['data']['prompt'];
|
|
error_log('TWP Voicemail: Using data.prompt: ' . $greeting);
|
|
} elseif (isset($step['prompt']) && !empty($step['prompt'])) {
|
|
$greeting = $step['prompt'];
|
|
error_log('TWP Voicemail: Using prompt: ' . $greeting);
|
|
} elseif (isset($step['data']['text']) && !empty($step['data']['text'])) {
|
|
$greeting = $step['data']['text'];
|
|
error_log('TWP Voicemail: Using data.text: ' . $greeting);
|
|
} elseif (isset($step['text']) && !empty($step['text'])) {
|
|
$greeting = $step['text'];
|
|
error_log('TWP Voicemail: Using text: ' . $greeting);
|
|
}
|
|
|
|
// Add greeting message if provided
|
|
if ($greeting) {
|
|
error_log('TWP Voicemail: Found greeting: ' . $greeting);
|
|
|
|
// Check for new audio_type structure or legacy use_tts
|
|
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
|
|
(isset($step['audio_type']) ? $step['audio_type'] :
|
|
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
|
|
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
|
|
error_log('TWP Voicemail: audio_type = ' . $audio_type);
|
|
|
|
switch ($audio_type) {
|
|
case 'tts':
|
|
error_log('TWP Voicemail: Attempting ElevenLabs TTS');
|
|
// Check for voice_id in data object or root
|
|
$voice_id = isset($step['data']['voice_id']) && !empty($step['data']['voice_id']) ? $step['data']['voice_id'] :
|
|
(isset($step['voice_id']) && !empty($step['voice_id']) ? $step['voice_id'] : null);
|
|
error_log('TWP Voicemail: voice_id = ' . ($voice_id ?: 'default'));
|
|
$audio_result = $elevenlabs->text_to_speech($greeting, [
|
|
'voice_id' => $voice_id
|
|
]);
|
|
|
|
if ($audio_result && isset($audio_result['success']) && $audio_result['success']) {
|
|
error_log('TWP Voicemail: ElevenLabs TTS successful, using audio file: ' . $audio_result['file_url']);
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
error_log('TWP Voicemail: ElevenLabs TTS failed, falling back to Say: ' . json_encode($audio_result));
|
|
$say = $twiml->addChild('Say', $greeting);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'audio':
|
|
// Use provided audio file
|
|
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
|
|
(isset($step['audio_url']) ? $step['audio_url'] : null);
|
|
|
|
if ($audio_url && !empty($audio_url)) {
|
|
error_log('TWP Voicemail: Using audio file: ' . $audio_url);
|
|
$play = $twiml->addChild('Play', $audio_url);
|
|
} else {
|
|
error_log('TWP Voicemail: No audio URL provided, falling back to Say');
|
|
$say = $twiml->addChild('Say', $greeting);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
default: // 'say' or fallback
|
|
error_log('TWP Voicemail: Using standard Say for greeting');
|
|
$say = $twiml->addChild('Say', $greeting);
|
|
$say->addAttribute('voice', 'alice');
|
|
break;
|
|
}
|
|
} else {
|
|
error_log('TWP Voicemail: No custom greeting found, using default');
|
|
// Default greeting if none provided
|
|
$say = $twiml->addChild('Say', 'Please leave your message after the beep. Press the pound key when finished.');
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
|
|
$record = $twiml->addChild('Record');
|
|
$record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
|
|
$record->addAttribute('playBeep', 'true');
|
|
$record->addAttribute('finishOnKey', '#');
|
|
$record->addAttribute('timeout', '10');
|
|
|
|
// Add action URL to handle what happens after recording
|
|
$action_url = home_url('/wp-json/twilio-webhook/v1/voicemail-complete');
|
|
$record->addAttribute('action', $action_url);
|
|
|
|
// Add recording status callback for saving the voicemail
|
|
$callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
|
|
if (isset($step['workflow_id'])) {
|
|
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
|
|
}
|
|
$record->addAttribute('recordingStatusCallback', $callback_url);
|
|
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
|
|
|
|
// Add transcription (enabled by default unless explicitly disabled)
|
|
if (!isset($step['transcribe']) || $step['transcribe'] !== false) {
|
|
$record->addAttribute('transcribe', 'true');
|
|
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
|
}
|
|
|
|
$twiml_output = $twiml->asXML();
|
|
error_log('TWP Voicemail: Generated TwiML: ' . $twiml_output);
|
|
return $twiml_output;
|
|
}
|
|
|
|
/**
|
|
* Handle schedule check
|
|
*/
|
|
private static function handle_schedule_check($step, $call_data) {
|
|
$schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
|
|
|
|
error_log('TWP Schedule Check: Processing schedule check with ID: ' . ($schedule_id ?: 'none'));
|
|
error_log('TWP Schedule Check: Step data: ' . json_encode($step));
|
|
|
|
if (!$schedule_id) {
|
|
error_log('TWP Schedule Check: No schedule ID specified, continuing to next step');
|
|
// No schedule specified, return false to continue to next step
|
|
return false;
|
|
}
|
|
|
|
// Check if we're within business hours first
|
|
$is_active = TWP_Scheduler::is_schedule_active($schedule_id);
|
|
error_log('TWP Schedule Check: Schedule active status: ' . ($is_active ? 'true' : 'false'));
|
|
|
|
if ($is_active) {
|
|
error_log('TWP Schedule Check: Within business hours, continuing to next workflow step');
|
|
// Within business hours - continue with normal workflow
|
|
return false; // Continue to next workflow step
|
|
} else {
|
|
error_log('TWP Schedule Check: Outside business hours, checking for after-hours steps');
|
|
// After hours - execute after-hours steps
|
|
$after_hours_steps = null;
|
|
|
|
if (isset($step['data']['after_hours_steps']) && !empty($step['data']['after_hours_steps'])) {
|
|
$after_hours_steps = $step['data']['after_hours_steps'];
|
|
} elseif (isset($step['after_hours_steps']) && !empty($step['after_hours_steps'])) {
|
|
$after_hours_steps = $step['after_hours_steps'];
|
|
}
|
|
|
|
if ($after_hours_steps) {
|
|
error_log('TWP Schedule Check: Found after-hours steps, executing: ' . json_encode($after_hours_steps));
|
|
return self::execute_after_hours_steps($after_hours_steps, $call_data);
|
|
} else {
|
|
error_log('TWP Schedule Check: No after-hours steps configured');
|
|
|
|
// Fall back to schedule routing if no after-hours steps in workflow
|
|
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
|
|
|
|
if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
|
|
error_log('TWP Schedule Check: Using schedule routing to workflow: ' . $routing['data']['workflow_id']);
|
|
// Route to different workflow
|
|
$workflow_id = $routing['data']['workflow_id'];
|
|
$workflow = self::get_workflow($workflow_id);
|
|
|
|
if ($workflow && $workflow->is_active) {
|
|
return self::execute_workflow($workflow_id, $call_data);
|
|
}
|
|
} else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
|
|
error_log('TWP Schedule Check: Using schedule routing to forward: ' . $routing['data']['forward_number']);
|
|
// Forward call
|
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
|
$twiml->dial($routing['data']['forward_number']);
|
|
return $twiml->asXML();
|
|
}
|
|
}
|
|
}
|
|
|
|
error_log('TWP Schedule Check: No action taken, continuing to next step');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Execute after-hours steps
|
|
*/
|
|
private static function execute_after_hours_steps($steps, $call_data) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
foreach ($steps as $step) {
|
|
switch ($step['type']) {
|
|
case 'greeting':
|
|
if (isset($step['message']) && !empty($step['message'])) {
|
|
$say = $twiml->addChild('Say', $step['message']);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'forward':
|
|
if (isset($step['number']) && !empty($step['number'])) {
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('answerOnBridge', 'true');
|
|
$dial->addChild('Number', $step['number']);
|
|
return $twiml->asXML(); // End here for forward
|
|
}
|
|
break;
|
|
|
|
case 'voicemail':
|
|
// Add greeting if provided
|
|
if (isset($step['greeting']) && !empty($step['greeting'])) {
|
|
$say = $twiml->addChild('Say', $step['greeting']);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
|
|
// Add record
|
|
$record = $twiml->addChild('Record');
|
|
$record->addAttribute('maxLength', '120');
|
|
$record->addAttribute('playBeep', 'true');
|
|
$record->addAttribute('finishOnKey', '#');
|
|
$record->addAttribute('timeout', '10');
|
|
$record->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/voicemail-complete'));
|
|
$record->addAttribute('recordingStatusCallback', home_url('/wp-json/twilio-webhook/v1/voicemail-callback'));
|
|
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
|
|
$record->addAttribute('transcribe', 'true');
|
|
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
|
return $twiml->asXML(); // End here for voicemail
|
|
|
|
case 'queue':
|
|
if (isset($step['queue_name']) && !empty($step['queue_name'])) {
|
|
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
|
|
return $twiml->asXML(); // End here for queue
|
|
}
|
|
break;
|
|
|
|
case 'sms':
|
|
if (isset($step['to_number']) && !empty($step['to_number']) &&
|
|
isset($step['message']) && !empty($step['message'])) {
|
|
// Send SMS notification
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
// Get the from number using proper priority
|
|
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
|
|
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
|
|
|
|
$message = str_replace(
|
|
array('{from}', '{to}', '{time}'),
|
|
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
|
$step['message']
|
|
);
|
|
$twilio->send_sms($step['to_number'], $message, $from_number);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Execute action
|
|
*/
|
|
private static function execute_action($action, $call_data) {
|
|
switch ($action['type']) {
|
|
case 'forward':
|
|
return self::create_forward_twiml($action);
|
|
|
|
case 'voicemail':
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
return self::create_voicemail_twiml($action, $elevenlabs);
|
|
|
|
case 'queue':
|
|
return self::create_queue_twiml($action);
|
|
|
|
case 'ring_group':
|
|
return self::create_ring_group_twiml($action);
|
|
|
|
case 'message':
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$say = $twiml->addChild('Say', $action['message']);
|
|
$say->addAttribute('voice', 'alice');
|
|
return $twiml->asXML();
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check conditions
|
|
*/
|
|
private static function check_conditions($conditions, $call_data) {
|
|
foreach ($conditions as $condition) {
|
|
switch ($condition['type']) {
|
|
case 'time':
|
|
$current_time = current_time('H:i');
|
|
if ($current_time < $condition['start_time'] || $current_time > $condition['end_time']) {
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'day_of_week':
|
|
$current_day = strtolower(date('l'));
|
|
if (!in_array($current_day, $condition['days'])) {
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'caller_id':
|
|
if (!in_array($call_data['From'], $condition['numbers'])) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Send SMS notification
|
|
*/
|
|
private static function send_sms_notification($step, $call_data) {
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
// Get the from number - priority: workflow phone > default SMS number > first Twilio number
|
|
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
|
|
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
|
|
|
|
$message = str_replace(
|
|
array('{from}', '{to}', '{time}'),
|
|
array($call_data['From'], $call_data['To'], current_time('g:i A')),
|
|
$step['message']
|
|
);
|
|
|
|
$twilio->send_sms($step['to_number'], $message, $from_number);
|
|
}
|
|
|
|
/**
|
|
* Create default response
|
|
*/
|
|
private static function create_default_response() {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.');
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Hangup');
|
|
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
/**
|
|
* Get workflow
|
|
*/
|
|
public static function get_workflow($workflow_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table_name WHERE id = %d",
|
|
$workflow_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get workflows
|
|
*/
|
|
public static function get_workflows() {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
|
|
}
|
|
|
|
/**
|
|
* Update workflow
|
|
*/
|
|
public static function update_workflow($workflow_id, $data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
$update_data = array();
|
|
$update_format = array();
|
|
|
|
if (isset($data['workflow_name'])) {
|
|
$update_data['workflow_name'] = sanitize_text_field($data['workflow_name']);
|
|
$update_format[] = '%s';
|
|
}
|
|
|
|
if (isset($data['phone_number'])) {
|
|
$update_data['phone_number'] = sanitize_text_field($data['phone_number']);
|
|
$update_format[] = '%s';
|
|
}
|
|
|
|
if (isset($data['workflow_data'])) {
|
|
// Check if workflow_data is already JSON string or needs encoding
|
|
if (is_string($data['workflow_data'])) {
|
|
$update_data['workflow_data'] = $data['workflow_data'];
|
|
} else {
|
|
$update_data['workflow_data'] = json_encode($data['workflow_data']);
|
|
}
|
|
$update_format[] = '%s';
|
|
}
|
|
|
|
if (isset($data['is_active'])) {
|
|
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
|
|
$update_format[] = '%d';
|
|
}
|
|
|
|
return $wpdb->update(
|
|
$table_name,
|
|
$update_data,
|
|
array('id' => $workflow_id),
|
|
$update_format,
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete workflow
|
|
*/
|
|
public static function delete_workflow($workflow_id) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
|
|
|
return $wpdb->delete(
|
|
$table_name,
|
|
array('id' => $workflow_id),
|
|
array('%d')
|
|
);
|
|
}
|
|
} |