Initiate outbound calls to connect customers with your phone. Click-to-call functionality allows you to dial any number.
Make an Outbound Call
Select the Twilio number to call from
Enter the number you want to call (include country code)
The number where you'll receive the call first
Recent Outbound Calls
Date/Time
From
To
Agent
Status
Duration
prefix . 'twp_call_log';
$recent_calls = $wpdb->get_results($wpdb->prepare("
SELECT cl.*, u.display_name as agent_name
FROM $log_table cl
LEFT JOIN {$wpdb->users} u ON JSON_EXTRACT(cl.actions_taken, '$.agent_id') = u.ID
WHERE cl.workflow_name = 'Outbound Call'
OR cl.status = 'outbound_initiated'
ORDER BY cl.created_at DESC
LIMIT 20
"));
if (empty($recent_calls)) {
echo '
prefix . 'twp_voicemails';
$workflows_table = $wpdb->prefix . 'twp_workflows';
$voicemails = $wpdb->get_results("
SELECT v.*, w.workflow_name
FROM $voicemails_table v
LEFT JOIN $workflows_table w ON v.workflow_id = w.id
ORDER BY v.created_at DESC
LIMIT 50
");
foreach ($voicemails as $voicemail) {
?>
';
}
}
/**
* Show admin notices
*/
public function show_admin_notices() {
// Check if we're on a plugin page
$screen = get_current_screen();
if (!$screen || strpos($screen->id, 'twilio-wp') === false) {
return;
}
// Check if database tables exist
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
$tables_exist = TWP_Activator::ensure_tables_exist();
if (!$tables_exist) {
?>
Twilio WP Plugin: Database tables were missing and have been created automatically.
If you continue to experience issues, please deactivate and reactivate the plugin.
Twilio WP Plugin: To use text-to-speech features, please configure your
ElevenLabs API key.
Twilio WP Plugin: Please configure your
Twilio credentials
to start using the plugin.
plugin_name,
TWP_PLUGIN_URL . 'assets/css/admin.css',
array('thickbox'),
$this->version,
'all'
);
}
/**
* Enqueue scripts
*/
public function enqueue_scripts() {
// Enqueue ThickBox for WordPress native modals
wp_enqueue_script('thickbox');
wp_enqueue_script(
$this->plugin_name,
TWP_PLUGIN_URL . 'assets/js/admin.js',
array('jquery', 'thickbox'),
$this->version,
false
);
wp_localize_script(
$this->plugin_name,
'twp_ajax',
array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('twp_ajax_nonce'),
'rest_url' => rest_url(),
'has_elevenlabs_key' => !empty(get_option('twp_elevenlabs_api_key')),
'timezone' => wp_timezone_string()
)
);
}
/**
* AJAX handler for saving schedule
*/
public function ajax_save_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Debug logging - log incoming POST data
error_log('TWP Schedule Save: POST data: ' . print_r($_POST, true));
$schedule_id = isset($_POST['schedule_id']) ? intval($_POST['schedule_id']) : 0;
// Remove duplicate days and sanitize
$days_of_week = isset($_POST['days_of_week']) ? $_POST['days_of_week'] : array();
$unique_days = array_unique(array_map('sanitize_text_field', $days_of_week));
$data = array(
'schedule_name' => sanitize_text_field($_POST['schedule_name']),
'days_of_week' => implode(',', $unique_days),
'start_time' => sanitize_text_field($_POST['start_time']),
'end_time' => sanitize_text_field($_POST['end_time']),
'workflow_id' => isset($_POST['workflow_id']) && !empty($_POST['workflow_id']) ? intval($_POST['workflow_id']) : null,
'holiday_dates' => isset($_POST['holiday_dates']) ? sanitize_textarea_field($_POST['holiday_dates']) : '',
'is_active' => isset($_POST['is_active']) ? 1 : 0
);
// Add optional fields if provided
if (!empty($_POST['phone_number'])) {
$data['phone_number'] = sanitize_text_field($_POST['phone_number']);
}
if (!empty($_POST['forward_number'])) {
$data['forward_number'] = sanitize_text_field($_POST['forward_number']);
}
if (!empty($_POST['after_hours_action'])) {
$data['after_hours_action'] = sanitize_text_field($_POST['after_hours_action']);
}
if (!empty($_POST['after_hours_workflow_id'])) {
$data['after_hours_workflow_id'] = intval($_POST['after_hours_workflow_id']);
}
if (!empty($_POST['after_hours_forward_number'])) {
$data['after_hours_forward_number'] = sanitize_text_field($_POST['after_hours_forward_number']);
}
// Debug logging - log processed data
error_log('TWP Schedule Save: Processed data: ' . print_r($data, true));
error_log('TWP Schedule Save: Schedule ID: ' . $schedule_id);
if ($schedule_id) {
error_log('TWP Schedule Save: Updating existing schedule');
$result = TWP_Scheduler::update_schedule($schedule_id, $data);
} else {
error_log('TWP Schedule Save: Creating new schedule');
$result = TWP_Scheduler::create_schedule($data);
}
error_log('TWP Schedule Save: Result: ' . ($result ? 'true' : 'false'));
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for deleting schedule
*/
public function ajax_delete_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedule_id = intval($_POST['schedule_id']);
$result = TWP_Scheduler::delete_schedule($schedule_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting all schedules
*/
public function ajax_get_schedules() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedules = TWP_Scheduler::get_schedules();
wp_send_json_success($schedules);
}
/**
* AJAX handler for getting a single schedule
*/
public function ajax_get_schedule() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$schedule_id = intval($_POST['schedule_id']);
$schedule = TWP_Scheduler::get_schedule($schedule_id);
if ($schedule) {
wp_send_json_success($schedule);
} else {
wp_send_json_error('Schedule not found');
}
}
/**
* AJAX handler for saving workflow
*/
public function ajax_save_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = isset($_POST['workflow_id']) ? intval($_POST['workflow_id']) : 0;
// Parse the workflow data JSON
$workflow_data_json = isset($_POST['workflow_data']) ? stripslashes($_POST['workflow_data']) : '{}';
// Log for debugging
error_log('TWP Workflow Save - Raw data: ' . $workflow_data_json);
// Handle empty workflow data
if (empty($workflow_data_json) || $workflow_data_json === '{}') {
$workflow_data_parsed = array(
'steps' => array(),
'conditions' => array(),
'actions' => array()
);
} else {
$workflow_data_parsed = json_decode($workflow_data_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('TWP Workflow Save - JSON Error: ' . json_last_error_msg());
wp_send_json_error('Invalid workflow data format: ' . json_last_error_msg());
return;
}
}
// Handle phone numbers - can be a single number (legacy) or array (new)
$phone_numbers = array();
if (isset($_POST['phone_numbers']) && is_array($_POST['phone_numbers'])) {
// New multi-number format
foreach ($_POST['phone_numbers'] as $number) {
$number = sanitize_text_field($number);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
} elseif (isset($_POST['phone_number'])) {
// Legacy single number format
$number = sanitize_text_field($_POST['phone_number']);
if (!empty($number)) {
$phone_numbers[] = $number;
}
}
$data = array(
'workflow_name' => sanitize_text_field($_POST['workflow_name']),
'phone_number' => isset($phone_numbers[0]) ? $phone_numbers[0] : '', // Keep first number for backward compatibility
'steps' => isset($workflow_data_parsed['steps']) ? $workflow_data_parsed['steps'] : array(),
'conditions' => isset($workflow_data_parsed['conditions']) ? $workflow_data_parsed['conditions'] : array(),
'actions' => isset($workflow_data_parsed['actions']) ? $workflow_data_parsed['actions'] : array(),
'is_active' => isset($_POST['is_active']) ? intval($_POST['is_active']) : 0,
'workflow_data' => $workflow_data_json // Keep the raw JSON for update_workflow
);
if ($workflow_id) {
$result = TWP_Workflow::update_workflow($workflow_id, $data);
} else {
$result = TWP_Workflow::create_workflow($data);
if ($result !== false) {
global $wpdb;
$workflow_id = $wpdb->insert_id;
}
}
// Save phone numbers to junction table
if ($result !== false && !empty($phone_numbers)) {
TWP_Workflow::set_workflow_phone_numbers($workflow_id, $phone_numbers);
}
if ($result === false) {
wp_send_json_error('Failed to save workflow to database');
} else {
wp_send_json_success(array('success' => true, 'workflow_id' => $workflow_id));
}
}
/**
* AJAX handler for getting workflow
*/
public function ajax_get_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$workflow = TWP_Workflow::get_workflow($workflow_id);
wp_send_json_success($workflow);
}
/**
* AJAX handler for getting workflow phone numbers
*/
public function ajax_get_workflow_phone_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Unauthorized');
return;
}
$workflow_id = intval($_POST['workflow_id']);
$phone_numbers = TWP_Workflow::get_workflow_phone_numbers($workflow_id);
wp_send_json_success($phone_numbers);
}
/**
* AJAX handler for deleting workflow
*/
public function ajax_delete_workflow() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$workflow_id = intval($_POST['workflow_id']);
$result = TWP_Workflow::delete_workflow($workflow_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting phone numbers
*/
public function ajax_get_phone_numbers() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_phone_numbers')) {
wp_send_json_error('Unauthorized - Phone number access required');
return;
}
$twilio = new TWP_Twilio_API();
$result = $twilio->get_phone_numbers();
if ($result['success']) {
wp_send_json_success($result['data']['incoming_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for searching available phone numbers
*/
public function ajax_search_available_numbers() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$country_code = sanitize_text_field($_POST['country_code']);
$area_code = sanitize_text_field($_POST['area_code']);
$contains = sanitize_text_field($_POST['contains']);
$twilio = new TWP_Twilio_API();
$result = $twilio->search_available_numbers($country_code, $area_code, $contains);
if ($result['success']) {
wp_send_json_success($result['data']['available_phone_numbers']);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for purchasing a phone number
*/
public function ajax_purchase_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$phone_number = sanitize_text_field($_POST['phone_number']);
$voice_url = isset($_POST['voice_url']) ? esc_url_raw($_POST['voice_url']) : null;
$sms_url = isset($_POST['sms_url']) ? esc_url_raw($_POST['sms_url']) : null;
$twilio = new TWP_Twilio_API();
$result = $twilio->purchase_phone_number($phone_number, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for configuring a phone number
*/
public function ajax_configure_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$voice_url = esc_url_raw($_POST['voice_url']);
$sms_url = esc_url_raw($_POST['sms_url']);
$twilio = new TWP_Twilio_API();
$result = $twilio->configure_phone_number($number_sid, $voice_url, $sms_url);
wp_send_json($result);
}
/**
* AJAX handler for releasing a phone number
*/
public function ajax_release_number() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$number_sid = sanitize_text_field($_POST['number_sid']);
$twilio = new TWP_Twilio_API();
$result = $twilio->release_phone_number($number_sid);
wp_send_json($result);
}
/**
* AJAX handler for getting queue details
*/
public function ajax_get_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = intval($_POST['queue_id']);
$queue = TWP_Call_Queue::get_queue($queue_id);
wp_send_json_success($queue);
}
/**
* AJAX handler for saving queue
*/
public function ajax_save_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : 0;
$data = array(
'queue_name' => sanitize_text_field($_POST['queue_name']),
'notification_number' => sanitize_text_field($_POST['notification_number']),
'agent_group_id' => !empty($_POST['agent_group_id']) ? intval($_POST['agent_group_id']) : null,
'max_size' => intval($_POST['max_size']),
'wait_music_url' => esc_url_raw($_POST['wait_music_url']),
'tts_message' => sanitize_textarea_field($_POST['tts_message']),
'timeout_seconds' => intval($_POST['timeout_seconds'])
);
if ($queue_id) {
// Update existing queue
$result = TWP_Call_Queue::update_queue($queue_id, $data);
} else {
// Create new queue
$result = TWP_Call_Queue::create_queue($data);
}
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting queue details with call info
*/
public function ajax_get_queue_details() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = intval($_POST['queue_id']);
$queue = TWP_Call_Queue::get_queue($queue_id);
if (!$queue) {
wp_send_json_error('Queue not found');
}
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Get current waiting calls
$waiting_calls = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $calls_table WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC",
$queue_id
));
// Calculate average wait time
$avg_wait = $wpdb->get_var($wpdb->prepare(
"SELECT AVG(TIMESTAMPDIFF(SECOND, joined_at, answered_at))
FROM $calls_table
WHERE queue_id = %d AND status = 'answered'
AND joined_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
$queue_id
));
$queue_status = TWP_Call_Queue::get_queue_status();
$waiting_count = 0;
foreach ($queue_status as $status) {
if ($status['queue_id'] == $queue_id) {
$waiting_count = $status['waiting_calls'];
break;
}
}
wp_send_json_success(array(
'queue' => $queue,
'waiting_calls' => $waiting_count,
'avg_wait_time' => $avg_wait ? round($avg_wait) . ' seconds' : 'N/A',
'calls' => $waiting_calls
));
}
/**
* AJAX handler for getting all queues
*/
public function ajax_get_all_queues() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queues = TWP_Call_Queue::get_all_queues();
wp_send_json_success($queues);
}
/**
* AJAX handler for deleting queue
*/
public function ajax_delete_queue() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$queue_id = intval($_POST['queue_id']);
$result = TWP_Call_Queue::delete_queue($queue_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for dashboard stats
*/
public function ajax_get_dashboard_stats() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Ensure database tables exist
require_once TWP_PLUGIN_DIR . 'includes/class-twp-activator.php';
$tables_exist = TWP_Activator::ensure_tables_exist();
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$log_table = $wpdb->prefix . 'twp_call_log';
$active_calls = 0;
$queued_calls = 0;
$recent_calls = array();
try {
// Check if tables exist before querying
$calls_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $calls_table));
$log_table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $log_table));
if ($calls_table_exists) {
// First, clean up old answered calls that might be stuck (older than 2 hours)
$wpdb->query(
"UPDATE $calls_table
SET status = 'completed', ended_at = NOW()
WHERE status = 'answered'
AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)"
);
// Get active calls - only recent ones to avoid counting stuck records
$active_calls = $wpdb->get_var(
"SELECT COUNT(*) FROM $calls_table
WHERE status IN ('waiting', 'answered')
AND joined_at >= DATE_SUB(NOW(), INTERVAL 4 HOUR)"
);
// Get queued calls
$queued_calls = $wpdb->get_var(
"SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'"
);
}
if ($log_table_exists) {
// Get recent calls from last 24 hours with phone numbers
$recent_calls = $wpdb->get_results(
"SELECT call_sid, from_number, to_number, status, duration, updated_at
FROM $log_table
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY updated_at DESC
LIMIT 10"
);
}
} catch (Exception $e) {
error_log('TWP Plugin Dashboard Stats Error: ' . $e->getMessage());
// Continue with default values
}
$formatted_calls = array();
foreach ($recent_calls as $call) {
// Format phone numbers for display
$from_display = $call->from_number ?: 'Unknown';
$to_display = $call->to_number ?: 'Unknown';
$formatted_calls[] = array(
'time' => $this->format_timestamp_with_timezone($call->updated_at, 'H:i'),
'from' => $from_display,
'to' => $to_display,
'status' => ucfirst($call->status),
'duration' => $call->duration ? $call->duration . 's' : '-'
);
}
wp_send_json_success(array(
'active_calls' => $active_calls ?: 0,
'queued_calls' => $queued_calls ?: 0,
'recent_calls' => $formatted_calls
));
}
/**
* AJAX handler for getting Eleven Labs voices
*/
public function ajax_get_elevenlabs_voices() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_cached_voices();
if ($result['success']) {
wp_send_json_success($result['data']['voices']);
} else {
$error_message = 'Failed to load voices';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
// Check if it's an API key issue and provide better error messages
if (empty(get_option('twp_elevenlabs_api_key'))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.';
} elseif (strpos(strtolower($error_message), 'unauthorized') !== false ||
strpos(strtolower($error_message), 'invalid') !== false ||
strpos(strtolower($error_message), '401') !== false) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.';
} elseif (strpos(strtolower($error_message), 'quota') !== false ||
strpos(strtolower($error_message), 'limit') !== false) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.';
} elseif (strpos(strtolower($error_message), 'network') !== false ||
strpos(strtolower($error_message), 'timeout') !== false ||
strpos(strtolower($error_message), 'connection') !== false) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.';
} elseif ($error_message === 'Failed to load voices') {
// Generic error - provide more helpful message
$api_key = get_option('twp_elevenlabs_api_key');
if (empty($api_key)) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.';
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.';
}
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler for getting ElevenLabs models
*/
public function ajax_get_elevenlabs_models() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_cached_models();
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
$error_message = 'Failed to load models';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
// Check if it's an API key issue and provide better error messages
if (empty(get_option('twp_elevenlabs_api_key'))) {
$error_message = 'Please configure your ElevenLabs API key in the settings first.';
} elseif (strpos(strtolower($error_message), 'unauthorized') !== false ||
strpos(strtolower($error_message), 'invalid') !== false ||
strpos(strtolower($error_message), '401') !== false) {
$error_message = 'Invalid API key. Please check your ElevenLabs API key in the settings.';
} elseif (strpos(strtolower($error_message), 'quota') !== false ||
strpos(strtolower($error_message), 'limit') !== false) {
$error_message = 'API quota exceeded. Please check your ElevenLabs subscription limits.';
} elseif (strpos(strtolower($error_message), 'network') !== false ||
strpos(strtolower($error_message), 'timeout') !== false ||
strpos(strtolower($error_message), 'connection') !== false) {
$error_message = 'Network error connecting to ElevenLabs. Please try again later.';
} elseif ($error_message === 'Failed to load models') {
// Generic error - provide more helpful message
$api_key = get_option('twp_elevenlabs_api_key');
if (empty($api_key)) {
$error_message = 'No ElevenLabs API key configured. Please add your API key in the settings.';
} else {
$error_message = 'Unable to connect to ElevenLabs API. Please check your API key and internet connection.';
}
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler for previewing a voice
*/
public function ajax_preview_voice() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$voice_id = sanitize_text_field($_POST['voice_id']);
$text = sanitize_text_field($_POST['text']) ?: 'Hello, this is a preview of this voice.';
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->text_to_speech($text, $voice_id);
if ($result['success']) {
wp_send_json_success(array(
'audio_url' => $result['file_url']
));
} else {
$error_message = 'Failed to generate voice preview';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
} elseif (is_array($result['error']) && isset($result['error']['error'])) {
$error_message = $result['error']['error'];
}
wp_send_json_error($error_message);
}
}
/**
* AJAX handler to get voicemail details
*/
public function ajax_get_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
if ($voicemail) {
wp_send_json_success($voicemail);
} else {
wp_send_json_error('Voicemail not found');
}
}
/**
* AJAX handler to delete voicemail
*/
public function ajax_delete_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$result = $wpdb->delete(
$table_name,
array('id' => $voicemail_id),
array('%d')
);
if ($result !== false) {
wp_send_json_success('Voicemail deleted successfully');
} else {
wp_send_json_error('Error deleting voicemail');
}
}
/**
* AJAX handler to get voicemail audio URL
*/
public function ajax_get_voicemail_audio() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
$voicemail_id = isset($_POST['voicemail_id']) ? intval($_POST['voicemail_id']) : 0;
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT recording_url FROM $table_name WHERE id = %d",
$voicemail_id
));
if (!$voicemail || !$voicemail->recording_url) {
wp_send_json_error('Voicemail not found');
return;
}
// Fetch the audio from Twilio using authenticated request
$account_sid = get_option('twp_twilio_account_sid');
$auth_token = get_option('twp_twilio_auth_token');
// Add .mp3 to the URL if not present
$audio_url = $voicemail->recording_url;
if (strpos($audio_url, '.mp3') === false && strpos($audio_url, '.wav') === false) {
$audio_url .= '.mp3';
}
// Log for debugging
error_log('TWP Voicemail Audio - Fetching from: ' . $audio_url);
// Fetch audio with authentication
$response = wp_remote_get($audio_url, array(
'headers' => array(
'Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)
),
'timeout' => 30
));
if (is_wp_error($response)) {
error_log('TWP Voicemail Audio - Error: ' . $response->get_error_message());
wp_send_json_error('Unable to fetch audio: ' . $response->get_error_message());
return;
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
error_log('TWP Voicemail Audio - HTTP Error: ' . $response_code);
wp_send_json_error('Audio fetch failed with code: ' . $response_code);
return;
}
$body = wp_remote_retrieve_body($response);
$content_type = wp_remote_retrieve_header($response, 'content-type') ?: 'audio/mpeg';
// Return audio as base64 data URL
$base64_audio = base64_encode($body);
$data_url = 'data:' . $content_type . ';base64,' . $base64_audio;
wp_send_json_success(array(
'audio_url' => $data_url,
'content_type' => $content_type,
'size' => strlen($body)
));
}
/**
* AJAX handler to manually transcribe voicemail
*/
public function ajax_transcribe_voicemail() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$voicemail_id = intval($_POST['voicemail_id']);
if (!$voicemail_id) {
wp_send_json_error('Invalid voicemail ID');
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
$voicemail = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$voicemail_id
));
if (!$voicemail) {
wp_send_json_error('Voicemail not found');
}
// For now, we'll use a placeholder transcription since we'd need a speech-to-text service
// In a real implementation, you'd send the recording URL to a transcription service
$placeholder_transcription = "This is a placeholder transcription. In a production environment, this would be generated using a speech-to-text service like Google Cloud Speech-to-Text, Amazon Transcribe, or Twilio's built-in transcription service.";
$result = $wpdb->update(
$table_name,
array('transcription' => $placeholder_transcription),
array('id' => $voicemail_id),
array('%s'),
array('%d')
);
if ($result !== false) {
wp_send_json_success(array('transcription' => $placeholder_transcription));
} else {
wp_send_json_error('Error generating transcription');
}
}
/**
* AJAX handler for getting user's recent voicemails
*/
public function ajax_get_user_voicemails() {
check_ajax_referer('twp_frontend_nonce', 'nonce');
if (!current_user_can('manage_options') && !current_user_can('twp_access_voicemails')) {
wp_send_json_error('Unauthorized');
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'twp_voicemails';
// Get recent voicemails (last 10)
$voicemails = $wpdb->get_results($wpdb->prepare("
SELECT id, from_number, duration, transcription, created_at, recording_url
FROM $table_name
ORDER BY created_at DESC
LIMIT %d
", 10));
// Format data for frontend
$formatted_voicemails = array();
foreach ($voicemails as $vm) {
$formatted_voicemails[] = array(
'id' => $vm->id,
'from_number' => $vm->from_number,
'duration' => $vm->duration,
'transcription' => $vm->transcription ? substr($vm->transcription, 0, 100) . '...' : 'No transcription',
'created_at' => $vm->created_at,
'time_ago' => human_time_diff(strtotime($vm->created_at), current_time('timestamp')) . ' ago',
'has_recording' => !empty($vm->recording_url)
);
}
// Get voicemail counts
$total_count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
$today_count = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*) FROM $table_name
WHERE DATE(created_at) = %s
", current_time('Y-m-d')));
wp_send_json_success(array(
'voicemails' => $formatted_voicemails,
'total_count' => $total_count,
'today_count' => $today_count
));
}
/**
* AJAX handler for getting all groups
*/
public function ajax_get_all_groups() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$groups = TWP_Agent_Groups::get_all_groups();
wp_send_json_success($groups);
}
/**
* AJAX handler for getting a group
*/
public function ajax_get_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$group = TWP_Agent_Groups::get_group($group_id);
wp_send_json_success($group);
}
/**
* AJAX handler for saving a group
*/
public function ajax_save_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = isset($_POST['group_id']) ? intval($_POST['group_id']) : 0;
$data = array(
'group_name' => sanitize_text_field($_POST['group_name']),
'description' => sanitize_textarea_field($_POST['description']),
'ring_strategy' => sanitize_text_field($_POST['ring_strategy'] ?? 'simultaneous'),
'timeout_seconds' => intval($_POST['timeout_seconds'] ?? 30)
);
if ($group_id) {
$result = TWP_Agent_Groups::update_group($group_id, $data);
} else {
$result = TWP_Agent_Groups::create_group($data);
}
wp_send_json_success(array('success' => $result !== false, 'group_id' => $result));
}
/**
* AJAX handler for deleting a group
*/
public function ajax_delete_group() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$result = TWP_Agent_Groups::delete_group($group_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting group members
*/
public function ajax_get_group_members() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$members = TWP_Agent_Groups::get_group_members($group_id);
wp_send_json_success($members);
}
/**
* AJAX handler for adding a group member
*/
public function ajax_add_group_member() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$user_id = intval($_POST['user_id']);
$priority = intval($_POST['priority'] ?? 0);
$result = TWP_Agent_Groups::add_member($group_id, $user_id, $priority);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for removing a group member
*/
public function ajax_remove_group_member() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$group_id = intval($_POST['group_id']);
$user_id = intval($_POST['user_id']);
$result = TWP_Agent_Groups::remove_member($group_id, $user_id);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for accepting a call
*/
public function ajax_accept_call() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$call_id = intval($_POST['call_id']);
$user_id = get_current_user_id();
$result = TWP_Agent_Manager::accept_queued_call($call_id, $user_id);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for accepting next call from a queue
*/
public function ajax_accept_next_queue_call() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$queue_id = intval($_POST['queue_id']);
$user_id = get_current_user_id();
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$groups_table = $wpdb->prefix . 'twp_group_members';
$queues_table = $wpdb->prefix . 'twp_call_queues';
// Verify user is a member of this queue's agent group
$is_member = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(*)
FROM $groups_table gm
JOIN $queues_table q ON gm.group_id = q.agent_group_id
WHERE gm.user_id = %d AND q.id = %d
", $user_id, $queue_id));
if (!$is_member) {
wp_send_json_error('You are not authorized to accept calls from this queue');
return;
}
// Get the next waiting call from this queue (lowest position number)
$next_call = $wpdb->get_row($wpdb->prepare("
SELECT * FROM $calls_table
WHERE queue_id = %d AND status = 'waiting'
ORDER BY position ASC
LIMIT 1
", $queue_id));
if (!$next_call) {
wp_send_json_error('No calls waiting in this queue');
return;
}
$result = TWP_Agent_Manager::accept_queued_call($next_call->id, $user_id);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result['error']);
}
}
/**
* AJAX handler for getting waiting calls
*/
public function ajax_get_waiting_calls() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$queues_table = $wpdb->prefix . 'twp_call_queues';
$groups_table = $wpdb->prefix . 'twp_group_members';
$user_id = get_current_user_id();
// Get waiting calls only from queues the user is a member of
$waiting_calls = $wpdb->get_results($wpdb->prepare("
SELECT
c.*,
q.queue_name,
TIMESTAMPDIFF(SECOND, c.joined_at, NOW()) as wait_seconds
FROM $calls_table c
JOIN $queues_table q ON c.queue_id = q.id
JOIN $groups_table gm ON gm.group_id = q.agent_group_id
WHERE c.status = 'waiting' AND gm.user_id = %d
ORDER BY c.position ASC
", $user_id));
wp_send_json_success($waiting_calls);
}
/**
* AJAX handler for getting agent's assigned queues
*/
public function ajax_get_agent_queues() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
wp_send_json_error('Unauthorized - Agent queue access required');
return;
}
global $wpdb;
$user_id = get_current_user_id();
$queues_table = $wpdb->prefix . 'twp_call_queues';
$groups_table = $wpdb->prefix . 'twp_group_members';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Get queues where user is a member of the assigned agent group
$user_queues = $wpdb->get_results($wpdb->prepare("
SELECT DISTINCT q.*,
COUNT(c.id) as waiting_count,
COALESCE(SUM(CASE WHEN c.status = 'waiting' THEN 1 ELSE 0 END), 0) as current_waiting
FROM $queues_table q
LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id
LEFT JOIN $calls_table c ON c.queue_id = q.id AND c.status = 'waiting'
WHERE gm.user_id = %d AND gm.is_active = 1
GROUP BY q.id
ORDER BY q.queue_name ASC
", $user_id));
wp_send_json_success($user_queues);
}
/**
* AJAX handler for setting agent status
*/
public function ajax_set_agent_status() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$user_id = get_current_user_id();
$status = sanitize_text_field($_POST['status']);
$result = TWP_Agent_Manager::set_agent_status($user_id, $status);
wp_send_json_success(array('success' => $result));
}
/**
* AJAX handler for getting call details
*/
public function ajax_get_call_details() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!isset($_POST['call_sid'])) {
wp_send_json_error('Call SID is required');
}
$call_sid = sanitize_text_field($_POST['call_sid']);
global $wpdb;
$table_name = $wpdb->prefix . 'twp_call_log';
$call = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE call_sid = %s",
$call_sid
));
if ($call) {
// Parse actions_taken if it's JSON
if ($call->actions_taken && is_string($call->actions_taken)) {
$decoded = json_decode($call->actions_taken, true);
if ($decoded) {
$call->actions_taken = json_encode($decoded, JSON_PRETTY_PRINT);
}
}
wp_send_json_success($call);
} else {
wp_send_json_error('Call not found');
}
}
/**
* AJAX handler for requesting callback
*/
public function ajax_request_callback() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$phone_number = sanitize_text_field($_POST['phone_number']);
$queue_id = isset($_POST['queue_id']) ? intval($_POST['queue_id']) : null;
$call_sid = isset($_POST['call_sid']) ? sanitize_text_field($_POST['call_sid']) : null;
if (empty($phone_number)) {
wp_send_json_error(array('message' => 'Phone number is required'));
}
$callback_id = TWP_Callback_Manager::request_callback($phone_number, $queue_id, $call_sid);
if ($callback_id) {
wp_send_json_success(array(
'callback_id' => $callback_id,
'message' => 'Callback requested successfully'
));
} else {
wp_send_json_error(array('message' => 'Failed to request callback'));
}
}
/**
* AJAX handler for initiating outbound calls
*/
public function ajax_initiate_outbound_call() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$to_number = sanitize_text_field($_POST['to_number']);
$agent_user_id = get_current_user_id();
if (empty($to_number)) {
wp_send_json_error(array('message' => 'Phone number is required'));
}
$result = TWP_Callback_Manager::initiate_outbound_call($to_number, $agent_user_id);
if ($result['success']) {
wp_send_json_success(array(
'call_sid' => $result['call_sid'],
'message' => 'Outbound call initiated successfully'
));
} else {
wp_send_json_error(array('message' => $result['error']));
}
}
/**
* AJAX handler for getting pending callbacks
*/
public function ajax_get_callbacks() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$pending_callbacks = TWP_Callback_Manager::get_pending_callbacks();
$callback_stats = TWP_Callback_Manager::get_callback_stats();
wp_send_json_success(array(
'callbacks' => $pending_callbacks,
'stats' => $callback_stats
));
}
/**
* AJAX handler for updating phone numbers with status callbacks
*/
public function ajax_update_phone_status_callbacks() {
check_ajax_referer('twp_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
try {
$twilio = new TWP_Twilio_API();
$result = $twilio->enable_status_callbacks_for_all_numbers();
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to update phone numbers: ' . $e->getMessage());
}
}
/**
* AJAX handler for toggling individual phone number status callbacks
*/
public function ajax_toggle_number_status_callback() {
check_ajax_referer('twp_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$sid = isset($_POST['sid']) ? sanitize_text_field($_POST['sid']) : '';
$enable = isset($_POST['enable']) ? $_POST['enable'] === 'true' : false;
if (empty($sid)) {
wp_send_json_error('Phone number SID is required');
}
try {
$twilio = new TWP_Twilio_API();
$result = $twilio->toggle_number_status_callback($sid, $enable);
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to update phone number: ' . $e->getMessage());
}
}
/**
* AJAX handler for generating capability tokens for Browser Phone
*/
public function ajax_generate_capability_token() {
// Check for either admin or frontend nonce
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
if (!current_user_can('manage_options') && !current_user_can('twp_access_browser_phone')) {
wp_send_json_error('Insufficient permissions');
}
try {
$twilio = new TWP_Twilio_API();
$result = $twilio->generate_capability_token();
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to generate capability token: ' . $e->getMessage());
}
}
/**
* AJAX handler for saving user's call mode preference
*/
public function ajax_save_call_mode() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('read')) {
wp_send_json_error('Insufficient permissions');
}
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : '';
if (!in_array($mode, ['browser', 'cell'])) {
wp_send_json_error('Invalid mode');
}
$user_id = get_current_user_id();
$updated = update_user_meta($user_id, 'twp_call_mode', $mode);
if ($updated !== false) {
wp_send_json_success([
'mode' => $mode,
'message' => 'Call mode updated successfully'
]);
} else {
wp_send_json_error('Failed to update call mode');
}
}
/**
* AJAX handler for auto-configuring TwiML App for browser phone
*/
public function ajax_auto_configure_twiml_app() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true';
$selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : [];
try {
$result = $this->auto_configure_browser_phone($enable_smart_routing, $selected_numbers);
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to auto-configure: ' . $e->getMessage());
}
}
/**
* Auto-configure browser phone by creating TwiML App and setting up webhooks
*/
private function auto_configure_browser_phone($enable_smart_routing = true, $selected_numbers = []) {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
if (!$client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Please check your credentials.'
];
}
$steps_completed = [];
$warnings = [];
try {
// Step 1: Check if TwiML App already exists
$current_app_sid = get_option('twp_twiml_app_sid');
$app_sid = null;
if ($current_app_sid) {
// Try to fetch existing app to verify it exists
try {
$existing_app = $client->applications($current_app_sid)->fetch();
$app_sid = $existing_app->sid;
$steps_completed[] = 'Found existing TwiML App: ' . $existing_app->friendlyName;
} catch (Exception $e) {
$warnings[] = 'Existing TwiML App SID is invalid, creating new one';
$current_app_sid = null;
}
}
// Step 2: Create TwiML App if needed
if (!$app_sid) {
$voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice');
$fallback_url = home_url('/wp-json/twilio-webhook/v1/browser-fallback');
$app = $client->applications->create([
'friendlyName' => 'Browser Phone App - ' . get_bloginfo('name'),
'voiceUrl' => $voice_url,
'voiceMethod' => 'POST',
'voiceFallbackUrl' => $fallback_url,
'voiceFallbackMethod' => 'POST'
]);
$app_sid = $app->sid;
$steps_completed[] = 'Created new TwiML App: ' . $app->friendlyName;
}
// Step 3: Save TwiML App SID to WordPress
update_option('twp_twiml_app_sid', $app_sid);
$steps_completed[] = 'Saved TwiML App SID to WordPress settings';
// Step 4: Test capability token generation
$token_result = $twilio->generate_capability_token();
if ($token_result['success']) {
$steps_completed[] = 'Successfully generated test capability token';
} else {
$warnings[] = 'Capability token generation failed: ' . $token_result['error'];
}
// Step 5: Update phone numbers with appropriate webhook URLs
$phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers);
if ($phone_result['updated_count'] > 0) {
$webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice';
$steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks';
}
if ($phone_result['skipped_count'] > 0) {
$steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)';
}
if (!empty($phone_result['warnings'])) {
$warnings = array_merge($warnings, $phone_result['warnings']);
}
return [
'success' => true,
'data' => [
'app_sid' => $app_sid,
'steps_completed' => $steps_completed,
'warnings' => $warnings,
'voice_url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'),
'message' => 'Browser phone auto-configuration completed successfully!'
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Auto-configuration failed: ' . $e->getMessage()
];
}
}
/**
* Auto-configure phone numbers with browser webhooks (optional)
*/
private function auto_configure_phone_numbers_for_browser($enable_smart_routing = true, $selected_numbers = []) {
$twilio = new TWP_Twilio_API();
$phone_numbers = $twilio->get_phone_numbers();
$updated_count = 0;
$skipped_count = 0;
$warnings = [];
if (!$phone_numbers['success']) {
return [
'updated_count' => 0,
'skipped_count' => 0,
'warnings' => ['Could not retrieve phone numbers: ' . $phone_numbers['error']]
];
}
// Create a map of selected number SIDs for quick lookup
$selected_sids = [];
if (!empty($selected_numbers)) {
foreach ($selected_numbers as $selected) {
$selected_sids[$selected['sid']] = true;
}
}
$smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing');
$browser_voice_url = home_url('/wp-json/twilio-webhook/v1/browser-voice');
$target_url = $enable_smart_routing ? $smart_routing_url : $browser_voice_url;
foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) {
// Skip if number is not selected (when selection is provided)
if (!empty($selected_numbers) && !isset($selected_sids[$number['sid']])) {
$skipped_count++;
error_log('TWP: Skipping phone number ' . $number['phone_number'] . ' (not selected)');
continue;
}
try {
// Only update if not already using the target URL
if ($number['voice_url'] !== $target_url) {
$client = $twilio->get_client();
$client->incomingPhoneNumbers($number['sid'])->update([
'voiceUrl' => $target_url,
'voiceMethod' => 'POST'
]);
$updated_count++;
error_log('TWP: Updated phone number ' . $number['phone_number'] . ' to use ' . $target_url);
}
} catch (Exception $e) {
$warnings[] = 'Failed to update ' . $number['phone_number'] . ': ' . $e->getMessage();
}
}
return [
'updated_count' => $updated_count,
'skipped_count' => $skipped_count,
'warnings' => $warnings
];
}
/**
* AJAX handler for configuring phone numbers only
*/
public function ajax_configure_phone_numbers_only() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$enable_smart_routing = isset($_POST['enable_smart_routing']) && $_POST['enable_smart_routing'] === 'true';
$selected_numbers = isset($_POST['selected_numbers']) ? json_decode(stripslashes($_POST['selected_numbers']), true) : [];
try {
$result = $this->configure_phone_numbers_only($enable_smart_routing, $selected_numbers);
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['error']);
}
} catch (Exception $e) {
wp_send_json_error('Failed to configure phone numbers: ' . $e->getMessage());
}
}
/**
* Configure phone numbers only (no TwiML App creation)
*/
private function configure_phone_numbers_only($enable_smart_routing = true, $selected_numbers = []) {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
if (!$client) {
return [
'success' => false,
'error' => 'Twilio client not initialized. Please check your credentials.'
];
}
$steps_completed = [];
$warnings = [];
try {
// Configure phone numbers
$phone_result = $this->auto_configure_phone_numbers_for_browser($enable_smart_routing, $selected_numbers);
if ($phone_result['updated_count'] > 0) {
$webhook_type = $enable_smart_routing ? 'smart routing' : 'browser voice';
$steps_completed[] = 'Updated ' . $phone_result['updated_count'] . ' phone numbers with ' . $webhook_type . ' webhooks';
} else {
$steps_completed[] = 'All selected phone numbers already configured correctly';
}
if ($phone_result['skipped_count'] > 0) {
$steps_completed[] = 'Skipped ' . $phone_result['skipped_count'] . ' phone numbers (not selected)';
}
if (!empty($phone_result['warnings'])) {
$warnings = array_merge($warnings, $phone_result['warnings']);
}
// If smart routing is enabled, verify TwiML App exists
if ($enable_smart_routing) {
$app_sid = get_option('twp_twiml_app_sid');
if (empty($app_sid)) {
$warnings[] = 'Smart routing enabled but no TwiML App SID configured. You may need to run full auto-configuration.';
} else {
// Test if the app exists
try {
$client->applications($app_sid)->fetch();
$steps_completed[] = 'Verified TwiML App exists for smart routing';
} catch (Exception $e) {
$warnings[] = 'TwiML App SID is invalid. Smart routing may not work properly.';
}
}
}
$webhook_url = $enable_smart_routing ?
home_url('/wp-json/twilio-webhook/v1/smart-routing') :
home_url('/wp-json/twilio-webhook/v1/browser-voice');
return [
'success' => true,
'data' => [
'steps_completed' => $steps_completed,
'warnings' => $warnings,
'webhook_url' => $webhook_url,
'routing_type' => $enable_smart_routing ? 'Smart Routing' : 'Direct Browser',
'message' => 'Phone number configuration completed successfully!'
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => 'Phone number configuration failed: ' . $e->getMessage()
];
}
}
/**
* AJAX handler for initiating outbound calls with from number
*/
public function ajax_initiate_outbound_call_with_from() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
$from_number = sanitize_text_field($_POST['from_number']);
$to_number = sanitize_text_field($_POST['to_number']);
$agent_phone = sanitize_text_field($_POST['agent_phone']);
if (empty($from_number) || empty($to_number) || empty($agent_phone)) {
wp_send_json_error(array('message' => 'All fields are required'));
}
// Validate phone numbers
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $to_number))) {
wp_send_json_error(array('message' => 'Invalid destination phone number format'));
}
if (!preg_match('/^\+?[1-9]\d{1,14}$/', str_replace([' ', '-', '(', ')'], '', $agent_phone))) {
wp_send_json_error(array('message' => 'Invalid agent phone number format'));
}
$result = $this->initiate_outbound_call_with_from($from_number, $to_number, $agent_phone);
if ($result['success']) {
wp_send_json_success(array(
'call_sid' => $result['call_sid'],
'message' => 'Outbound call initiated successfully'
));
} else {
wp_send_json_error(array('message' => $result['error']));
}
}
/**
* Initiate outbound call with specific from number
*/
private function initiate_outbound_call_with_from($from_number, $to_number, $agent_phone) {
$twilio = new TWP_Twilio_API();
// Build webhook URL with parameters
$webhook_url = home_url('/wp-json/twilio-webhook/v1/outbound-agent-with-from') . '?' . http_build_query(array(
'target_number' => $to_number,
'agent_user_id' => get_current_user_id(),
'from_number' => $from_number
));
// First call the agent
$agent_call_result = $twilio->make_call(
$agent_phone,
$webhook_url,
null, // No status callback needed for this
$from_number // Use specified from number
);
if ($agent_call_result['success']) {
$call_sid = isset($agent_call_result['data']['sid']) ? $agent_call_result['data']['sid'] : null;
// Set agent to busy
TWP_Agent_Manager::set_agent_status(get_current_user_id(), 'busy', $call_sid);
// Log the outbound call
TWP_Call_Logger::log_call(array(
'call_sid' => $call_sid,
'from_number' => $from_number,
'to_number' => $to_number,
'status' => 'outbound_initiated',
'workflow_name' => 'Outbound Call',
'actions_taken' => json_encode(array(
'agent_id' => get_current_user_id(),
'agent_name' => wp_get_current_user()->display_name,
'type' => 'click_to_call_with_from',
'agent_phone' => $agent_phone
))
));
return array('success' => true, 'call_sid' => $call_sid);
}
return array('success' => false, 'error' => $agent_call_result['error']);
}
/**
* Display SMS Inbox page
*/
public function display_sms_inbox_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_sms_log';
// Get our Twilio numbers first
$twilio_numbers = [];
try {
$twilio_api = new TWP_Twilio_API();
$numbers_result = $twilio_api->get_phone_numbers();
if ($numbers_result['success'] && !empty($numbers_result['data']['incoming_phone_numbers'])) {
foreach ($numbers_result['data']['incoming_phone_numbers'] as $number) {
$twilio_numbers[] = $number['phone_number'];
}
}
} catch (Exception $e) {
error_log('Failed to get Twilio numbers: ' . $e->getMessage());
}
// Build the NOT IN clause for Twilio numbers
$twilio_numbers_placeholders = !empty($twilio_numbers) ?
implode(',', array_fill(0, count($twilio_numbers), '%s')) :
"'dummy_number_that_wont_match'";
// Get unique conversations (group by customer phone number)
// Customer number is the one that's NOT in our Twilio numbers list
$query = $wpdb->prepare(
"SELECT
customer_number,
business_number,
MAX(last_message_time) as last_message_time,
SUM(message_count) as message_count,
MAX(last_message) as last_message,
MAX(last_direction) as last_message_direction
FROM (
SELECT
from_number as customer_number,
to_number as business_number,
MAX(received_at) as last_message_time,
COUNT(*) as message_count,
(SELECT body FROM $table_name t2
WHERE t2.from_number = t1.from_number AND t2.to_number = t1.to_number
ORDER BY t2.received_at DESC LIMIT 1) as last_message,
'incoming' as last_direction
FROM $table_name t1
WHERE from_number NOT IN ($twilio_numbers_placeholders)
AND body NOT IN ('1', 'status', 'help')
GROUP BY from_number, to_number
UNION ALL
SELECT
to_number as customer_number,
from_number as business_number,
MAX(received_at) as last_message_time,
COUNT(*) as message_count,
(SELECT body FROM $table_name t3
WHERE t3.to_number = t1.to_number AND t3.from_number = t1.from_number
ORDER BY t3.received_at DESC LIMIT 1) as last_message,
'outgoing' as last_direction
FROM $table_name t1
WHERE to_number NOT IN ($twilio_numbers_placeholders)
AND from_number IN ($twilio_numbers_placeholders)
GROUP BY to_number, from_number
) as conversations
GROUP BY customer_number
ORDER BY last_message_time DESC
LIMIT 50",
...$twilio_numbers,
...$twilio_numbers,
...$twilio_numbers
);
$conversations = $wpdb->get_results($query);
?>
SMS Inbox
View conversations and respond to customer SMS messages. Click on a conversation to view the full thread.
get_phone_numbers();
if (!$phone_numbers['success']) {
return false;
}
$smart_routing_url = home_url('/wp-json/twilio-webhook/v1/smart-routing');
foreach ($phone_numbers['data']['incoming_phone_numbers'] as $number) {
if ($number['voice_url'] === $smart_routing_url) {
return true;
}
}
return false;
} catch (Exception $e) {
error_log('TWP: Error checking smart routing status: ' . $e->getMessage());
return false;
}
}
/**
* Get user's queue memberships
*/
private function get_user_queue_memberships($user_id) {
global $wpdb;
// Get agent groups the user belongs to
$groups_table = $wpdb->prefix . 'twp_group_members';
$queues_table = $wpdb->prefix . 'twp_call_queues';
$user_groups = $wpdb->get_results($wpdb->prepare(
"SELECT gm.group_id, q.id as queue_id, q.queue_name
FROM $groups_table gm
JOIN $queues_table q ON gm.group_id = q.agent_group_id
WHERE gm.user_id = %d",
$user_id
));
$queues = [];
foreach ($user_groups as $group) {
$queues[$group->queue_id] = [
'id' => $group->queue_id,
'name' => $group->queue_name
];
}
return array_values($queues);
}
/**
* AJAX handler for toggling call hold
*/
public function ajax_toggle_hold() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$hold = filter_var($_POST['hold'], FILTER_VALIDATE_BOOLEAN);
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
if ($hold) {
// For browser phone calls, we need to find the customer's call leg and put THAT on hold
$hold_music_url = get_option('twp_hold_music_url', '');
if (empty($hold_music_url)) {
$hold_music_url = get_option('twp_default_queue_music_url', 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav');
}
// Get the call details to understand the call structure
$call = $client->calls($call_sid)->fetch();
error_log("TWP Hold: Initial call SID $call_sid - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: {$call->parentCallSid}");
// Determine which call to put on hold
$target_sid = null;
// Check if this is a browser phone (client) call
if (strpos($call->to, 'client:') === 0 || strpos($call->from, 'client:') === 0) {
// This is the browser phone leg - we need to find the customer leg
error_log("TWP Hold: Detected browser phone call, looking for customer leg");
// For outbound calls from browser phone, the structure is usually:
// - Browser phone initiates call (this call)
// - System dials customer (separate call with same parent or as child)
// First check if there's a parent call that might be a conference
if ($call->parentCallSid) {
try {
$parent_call = $client->calls($call->parentCallSid)->fetch();
error_log("TWP Hold: Parent call {$parent_call->sid} - From: {$parent_call->from}, To: {$parent_call->to}");
// Check if parent is the customer call (not a client call)
if (strpos($parent_call->to, 'client:') === false && strpos($parent_call->from, 'client:') === false) {
$target_sid = $parent_call->sid;
error_log("TWP Hold: Using parent call as target");
}
} catch (Exception $e) {
error_log("TWP Hold: Could not fetch parent: " . $e->getMessage());
}
}
// If no target found yet, search for related calls
if (!$target_sid) {
// Get all in-progress calls and find the related customer call
$active_calls = $client->calls->read(['status' => 'in-progress'], 50);
error_log("TWP Hold: Searching " . count($active_calls) . " active calls for customer leg");
foreach ($active_calls as $active_call) {
// Skip the current call
if ($active_call->sid === $call_sid) continue;
// Log each call for debugging
error_log("TWP Hold: Checking call {$active_call->sid} - From: {$active_call->from}, To: {$active_call->to}, Parent: {$active_call->parentCallSid}");
// Check if this call is related (same parent, or parent/child relationship)
$is_related = false;
if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) {
$is_related = true; // Same parent
} elseif ($active_call->parentCallSid === $call_sid) {
$is_related = true; // This call is child of our call
} elseif ($active_call->sid === $call->parentCallSid) {
$is_related = true; // This call is parent of our call
}
if ($is_related) {
// Make sure this is not a client call
if (strpos($active_call->to, 'client:') === false &&
strpos($active_call->from, 'client:') === false) {
$target_sid = $active_call->sid;
error_log("TWP Hold: Found related customer call: {$active_call->sid}");
break;
}
}
}
}
} else {
// This is not a client call, so it's likely the customer call itself
$target_sid = $call_sid;
error_log("TWP Hold: Using current call as target (not a client call)");
}
// Apply hold to the determined target
if ($target_sid) {
error_log("TWP Hold: Putting call on hold - Target SID: {$target_sid}");
// Create hold TwiML
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold while we assist you.', ['voice' => 'alice']);
$twiml->play($hold_music_url, ['loop' => 0]);
try {
$result = $client->calls($target_sid)->update([
'twiml' => $twiml->asXML()
]);
error_log("TWP Hold: Successfully updated call {$target_sid} with hold music");
} catch (Exception $e) {
error_log("TWP Hold: Error updating call: " . $e->getMessage());
throw $e;
}
} else {
error_log("TWP Hold: WARNING - Could not determine target call, putting current call on hold as fallback");
// Fallback: put current call on hold
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold.', ['voice' => 'alice']);
$twiml->play($hold_music_url, ['loop' => 0]);
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
}
} else {
// Resume call - use similar logic to find the right leg
$call = $client->calls($call_sid)->fetch();
error_log("TWP Resume: Initial call SID $call_sid - From: {$call->from}, To: {$call->to}");
$target_sid = null;
// Check if this is a browser phone call
if (strpos($call->to, 'client:') === 0 || strpos($call->from, 'client:') === 0) {
// Find the customer leg using same logic as hold
if ($call->parentCallSid) {
try {
$parent_call = $client->calls($call->parentCallSid)->fetch();
if (strpos($parent_call->to, 'client:') === false && strpos($parent_call->from, 'client:') === false) {
$target_sid = $parent_call->sid;
}
} catch (Exception $e) {
error_log("TWP Resume: Could not fetch parent: " . $e->getMessage());
}
}
if (!$target_sid) {
// Search for related customer call
$active_calls = $client->calls->read(['status' => 'in-progress'], 50);
foreach ($active_calls as $active_call) {
if ($active_call->sid === $call_sid) continue;
$is_related = false;
if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) {
$is_related = true;
} elseif ($active_call->parentCallSid === $call_sid) {
$is_related = true;
} elseif ($active_call->sid === $call->parentCallSid) {
$is_related = true;
}
if ($is_related && strpos($active_call->to, 'client:') === false &&
strpos($active_call->from, 'client:') === false) {
$target_sid = $active_call->sid;
error_log("TWP Resume: Found related customer call: {$active_call->sid}");
break;
}
}
}
} else {
$target_sid = $call_sid;
}
// Resume the determined target
if ($target_sid) {
error_log("TWP Resume: Resuming call - Target SID: {$target_sid}");
// For resuming, we need to stop the hold music and restore the conversation
// The key is to return control to the normal call flow
try {
// Get the target call details to understand the call structure
$target_call = $client->calls($target_sid)->fetch();
error_log("TWP Resume: Target call - From: {$target_call->from}, To: {$target_call->to}, Parent: {$target_call->parentCallSid}");
// For resuming, the simplest approach is often the best
// Just send empty TwiML to stop hold music and resume normal call flow
$twiml = new \Twilio\TwiML\VoiceResponse();
// Don't add anything - empty TwiML should resume the call
// The connection between parties should still exist
// Update the call with resume TwiML
$result = $client->calls($target_sid)->update([
'twiml' => $twiml->asXML()
]);
error_log("TWP Resume: Successfully updated call {$target_sid} with resume TwiML");
} catch (Exception $e) {
error_log("TWP Resume: Error resuming call: " . $e->getMessage());
// Simple fallback - just stop hold music with empty TwiML
$twiml = new \Twilio\TwiML\VoiceResponse();
$client->calls($target_sid)->update([
'twiml' => $twiml->asXML()
]);
error_log("TWP Resume: Used simple fallback for {$target_sid}");
}
} else {
error_log("TWP Resume: WARNING - Could not determine target, resuming current call");
// Resume current call as fallback
try {
$twiml = new \Twilio\TwiML\VoiceResponse();
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
} catch (Exception $e) {
error_log("TWP Resume: Failed to resume current call: " . $e->getMessage());
throw $e;
}
}
}
wp_send_json_success(['message' => $hold ? 'Call on hold' : 'Call resumed']);
} catch (Exception $e) {
wp_send_json_error('Failed to toggle hold: ' . $e->getMessage());
}
}
/**
* AJAX handler for getting available agents for transfer
*/
public function ajax_get_transfer_agents() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$users_table = $wpdb->prefix . 'users';
$usermeta_table = $wpdb->prefix . 'usermeta';
$status_table = $wpdb->prefix . 'twp_agent_status';
// Get all users with the twp_access_browser_phone capability or admins
$all_users = get_users([
'orderby' => 'display_name',
'order' => 'ASC'
]);
$agents = [];
$current_user_id = get_current_user_id();
foreach ($all_users as $user) {
// Skip current user
if ($user->ID == $current_user_id) {
continue;
}
// Check if user can access browser phone or is admin
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
continue;
}
// Get user's phone number
$phone_number = get_user_meta($user->ID, 'twp_phone_number', true);
// Get user's status
$status = $wpdb->get_var($wpdb->prepare(
"SELECT status FROM $status_table WHERE user_id = %d",
$user->ID
));
$agents[] = [
'id' => $user->ID,
'name' => $user->display_name,
'phone' => $phone_number,
'status' => $status ?: 'offline',
'has_phone' => !empty($phone_number),
'queue_name' => 'agent_' . $user->ID // Personal queue name
];
}
wp_send_json_success($agents);
}
/**
* AJAX handler for transferring a call
*/
public function ajax_transfer_call() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$transfer_type = sanitize_text_field($_POST['transfer_type']); // 'phone' or 'queue'
$transfer_target = sanitize_text_field($_POST['transfer_target']);
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Create TwiML based on transfer type
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Transferring your call. Please hold.');
if ($transfer_type === 'queue') {
// Transfer to agent's personal queue
$enqueue = $twiml->enqueue($transfer_target);
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
// Extract agent ID from queue name (format: agent_123)
if (preg_match('/agent_(\d+)/', $transfer_target, $matches)) {
$agent_id = intval($matches[1]);
// Add to personal queue tracking in database
global $wpdb;
$table = $wpdb->prefix . 'twp_personal_queue_calls';
// Create table if it doesn't exist
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table (
id int(11) NOT NULL AUTO_INCREMENT,
agent_id bigint(20) NOT NULL,
call_sid varchar(100) NOT NULL,
from_number varchar(20),
enqueued_at datetime DEFAULT CURRENT_TIMESTAMP,
status varchar(20) DEFAULT 'waiting',
PRIMARY KEY (id),
KEY agent_id (agent_id),
KEY call_sid (call_sid)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// Get call details
$call = $client->calls($call_sid)->fetch();
// Insert into personal queue tracking
$wpdb->insert($table, [
'agent_id' => $agent_id,
'call_sid' => $call_sid,
'from_number' => $call->from,
'status' => 'waiting'
]);
}
} else {
// Transfer to phone number
if (!preg_match('/^\+?[1-9]\d{1,14}$/', $transfer_target)) {
wp_send_json_error('Invalid phone number format');
return;
}
$twiml->dial($transfer_target);
}
// Update the call with the transfer TwiML
$call = $client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
wp_send_json_success(['message' => 'Call transferred successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
}
/**
* AJAX handler for requeuing a call
*/
public function ajax_requeue_call() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
// Validate queue exists
global $wpdb;
$queue_table = $wpdb->prefix . 'twp_call_queues';
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queue_table WHERE id = %d",
$queue_id
));
if (!$queue) {
wp_send_json_error('Invalid queue');
return;
}
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Create TwiML to enqueue the call
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Placing you back in the queue. Please hold.');
$enqueue = $twiml->enqueue($queue->queue_name);
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
// Update the call with the requeue TwiML
$call = $client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
// Add call to our database queue tracking
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Use enqueued_at if available, fallback to joined_at for compatibility
$insert_data = [
'queue_id' => $queue_id,
'call_sid' => $call_sid,
'from_number' => $call->from,
'to_number' => $call->to ?: '',
'position' => 1, // Will be updated by queue manager
'status' => 'waiting'
];
// Check if enqueued_at column exists
$columns = $wpdb->get_col("DESCRIBE $calls_table");
if (in_array('enqueued_at', $columns)) {
$insert_data['enqueued_at'] = current_time('mysql');
} else {
$insert_data['joined_at'] = current_time('mysql');
}
$wpdb->insert($calls_table, $insert_data);
wp_send_json_success(['message' => 'Call requeued successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to requeue call: ' . $e->getMessage());
}
}
/**
* AJAX handler for starting call recording
*/
public function ajax_start_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$user_id = get_current_user_id();
if (empty($call_sid)) {
wp_send_json_error('Call SID is required for recording');
return;
}
error_log("TWP: Starting recording for call SID: $call_sid");
// Ensure database table exists and run any migrations
TWP_Activator::ensure_tables_exist();
TWP_Activator::force_table_updates();
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// First, verify the call exists and is in progress
try {
$call = $client->calls($call_sid)->fetch();
error_log("TWP: Call found - Status: {$call->status}, From: {$call->from}, To: {$call->to}");
if (!in_array($call->status, ['in-progress', 'ringing'])) {
wp_send_json_error("Cannot record call in status: {$call->status}. Call must be in-progress.");
return;
}
} catch (Exception $call_error) {
error_log("TWP: Error fetching call details: " . $call_error->getMessage());
wp_send_json_error("Call not found or not accessible: " . $call_error->getMessage());
return;
}
// Start recording the call
$recording = $client->calls($call_sid)->recordings->create([
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/recording-status'),
'recordingStatusCallbackEvent' => ['completed', 'absent'],
'recordingChannels' => 'dual'
]);
error_log("TWP: Recording created with SID: {$recording->sid}");
// Store recording info in database
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// For outbound calls from browser phone, determine the customer number
$from_number = $call->from;
$to_number = $call->to;
error_log("TWP Recording: Initial call data - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}");
// If this is a browser phone call (from contains 'client:'), find the customer number
if (strpos($call->from, 'client:') === 0) {
// This is an outbound call from browser phone
error_log("TWP Recording: Detected browser phone outbound call");
// For browser phone calls, we need to find the customer number
// It might be in 'to' field, or we might need to look at related calls
$customer_number = null;
// First try the 'to' field
if (!empty($call->to) && strpos($call->to, 'client:') === false) {
$customer_number = $call->to;
error_log("TWP Recording: Found customer number in 'to' field: {$customer_number}");
} else {
// If 'to' is empty or also a client, look for related calls
error_log("TWP Recording: 'to' field empty or client, looking for related calls");
try {
$twilio = new TWP_Twilio_API();
$client_api = $twilio->get_client();
// Get all recent calls to find the customer leg
$related_calls = $client_api->calls->read(['status' => 'in-progress'], 20);
foreach ($related_calls as $related_call) {
// Skip the current call
if ($related_call->sid === $call_sid) continue;
// Look for calls with same parent or that are our parent/child
if (($call->parentCallSid && $related_call->parentCallSid === $call->parentCallSid) ||
$related_call->parentCallSid === $call_sid ||
$related_call->sid === $call->parentCallSid) {
// Check if this call has a real phone number (not client)
if (strpos($related_call->from, 'client:') === false &&
strpos($related_call->from, '+') === 0) {
$customer_number = $related_call->from;
error_log("TWP Recording: Found customer number in related call 'from': {$customer_number}");
break;
} elseif (strpos($related_call->to, 'client:') === false &&
strpos($related_call->to, '+') === 0) {
$customer_number = $related_call->to;
error_log("TWP Recording: Found customer number in related call 'to': {$customer_number}");
break;
}
}
}
} catch (Exception $e) {
error_log("TWP Recording: Error looking for related calls: " . $e->getMessage());
}
}
if ($customer_number) {
// Store customer number in 'from' for display purposes
$from_number = $customer_number;
$to_number = $call->from; // Browser phone client
error_log("TWP Recording: Outbound call - Customer: {$customer_number}, Agent: {$call->from}");
} else {
error_log("TWP Recording: WARNING - Could not determine customer number for outbound call");
// Keep original values but log the issue
}
}
$insert_result = $wpdb->insert($recordings_table, [
'call_sid' => $call_sid,
'recording_sid' => $recording->sid,
'from_number' => $from_number,
'to_number' => $to_number,
'agent_id' => $user_id,
'status' => 'recording',
'started_at' => current_time('mysql')
]);
if ($insert_result === false) {
error_log("TWP: Database insert failed: " . $wpdb->last_error);
wp_send_json_error("Failed to save recording to database: " . $wpdb->last_error);
return;
} else {
error_log("TWP: Recording saved to database - Recording SID: {$recording->sid}, Call SID: $call_sid");
}
wp_send_json_success([
'message' => 'Recording started',
'recording_sid' => $recording->sid,
'call_sid' => $call_sid // Include call_sid for debugging
]);
} catch (Exception $e) {
error_log("TWP: Recording start error: " . $e->getMessage());
wp_send_json_error('Failed to start recording: ' . $e->getMessage());
}
}
/**
* AJAX handler for stopping call recording
*/
public function ajax_stop_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$recording_sid = sanitize_text_field($_POST['recording_sid']);
if (empty($recording_sid)) {
wp_send_json_error('Recording SID is required');
return;
}
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Check if recording exists and is active
$recording_info = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $recordings_table WHERE recording_sid = %s",
$recording_sid
));
if (!$recording_info) {
error_log("TWP: Recording $recording_sid not found in database, attempting Twilio-only stop");
// Try to stop the recording in Twilio anyway (might exist there but not in DB)
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
$recording = $client->recordings($recording_sid)->update(['status' => 'stopped']);
error_log("TWP: Successfully stopped recording $recording_sid in Twilio despite DB issue");
wp_send_json_success(['message' => 'Recording stopped (was not tracked in database)']);
return;
} catch (Exception $twilio_error) {
error_log("TWP: Recording $recording_sid not found in database or Twilio: " . $twilio_error->getMessage());
wp_send_json_error('Recording not found in database or Twilio system');
return;
}
}
if ($recording_info->status === 'completed') {
// Already stopped, just update UI
wp_send_json_success(['message' => 'Recording already stopped']);
return;
}
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Try to stop the recording in Twilio
try {
$recording = $client->recordings($recording_sid)->update(['status' => 'stopped']);
} catch (Exception $twilio_error) {
// Recording might already be stopped or completed on Twilio's side
error_log('TWP: Could not stop recording in Twilio (may already be stopped): ' . $twilio_error->getMessage());
}
// Update database regardless
$wpdb->update(
$recordings_table,
[
'status' => 'completed',
'ended_at' => current_time('mysql')
],
['recording_sid' => $recording_sid]
);
wp_send_json_success(['message' => 'Recording stopped']);
} catch (Exception $e) {
error_log('TWP: Error stopping recording: ' . $e->getMessage());
wp_send_json_error('Failed to stop recording: ' . $e->getMessage());
}
}
/**
* AJAX handler for getting call recordings
*/
public function ajax_get_call_recordings() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
$user_id = get_current_user_id();
// Build query based on user permissions
if (current_user_can('manage_options')) {
// Admins can see all recordings
$recordings = $wpdb->get_results("
SELECT r.*, u.display_name as agent_name
FROM $recordings_table r
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
ORDER BY r.started_at DESC
LIMIT 100
");
} else {
// Regular users see only their recordings
$recordings = $wpdb->get_results($wpdb->prepare("
SELECT r.*, u.display_name as agent_name
FROM $recordings_table r
LEFT JOIN {$wpdb->users} u ON r.agent_id = u.ID
WHERE r.agent_id = %d
ORDER BY r.started_at DESC
LIMIT 50
", $user_id));
}
// Format recordings for display
$formatted_recordings = [];
foreach ($recordings as $recording) {
$formatted_recordings[] = [
'id' => $recording->id,
'call_sid' => $recording->call_sid,
'recording_sid' => $recording->recording_sid,
'from_number' => $recording->from_number,
'to_number' => $recording->to_number,
'agent_name' => $recording->agent_name,
'duration' => $recording->duration,
'started_at' => $recording->started_at,
'recording_url' => $recording->recording_url,
'has_recording' => !empty($recording->recording_url)
];
}
wp_send_json_success($formatted_recordings);
}
/**
* AJAX handler for deleting a recording (admin only)
*/
public function ajax_delete_recording() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
// Check admin permissions
if (!current_user_can('manage_options')) {
wp_send_json_error('You do not have permission to delete recordings');
return;
}
$recording_id = intval($_POST['recording_id']);
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
// Get recording details first
$recording = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $recordings_table WHERE id = %d",
$recording_id
));
if (!$recording) {
wp_send_json_error('Recording not found');
return;
}
// Delete from Twilio if we have a recording SID
if ($recording->recording_sid) {
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Try to delete from Twilio
$client->recordings($recording->recording_sid)->delete();
} catch (Exception $e) {
// Log error but continue with local deletion
error_log('TWP: Failed to delete recording from Twilio: ' . $e->getMessage());
}
}
// Delete from database
$result = $wpdb->delete(
$recordings_table,
['id' => $recording_id],
['%d']
);
if ($result === false) {
wp_send_json_error('Failed to delete recording from database');
} else {
wp_send_json_success(['message' => 'Recording deleted successfully']);
}
}
/**
* AJAX handler for getting online agents for transfer
*/
public function ajax_get_online_agents() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
// Get all agents with their status
$agents = $wpdb->get_results("
SELECT
u.ID,
u.display_name,
u.user_email,
um.meta_value as phone_number,
s.status,
s.current_call_sid,
CASE
WHEN s.status = 'available' AND s.current_call_sid IS NULL THEN 1
WHEN s.status = 'available' AND s.current_call_sid IS NOT NULL THEN 2
WHEN s.status = 'busy' THEN 3
ELSE 4
END as priority
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number'
LEFT JOIN $status_table s ON u.ID = s.user_id
WHERE u.ID != %d
ORDER BY priority, u.display_name
", get_current_user_id());
$formatted_agents = [];
foreach ($agents as $agent) {
$transfer_method = null;
$transfer_value = null;
// Determine transfer method
if ($agent->phone_number) {
$transfer_method = 'phone';
$transfer_value = $agent->phone_number;
} elseif ($agent->status === 'available') {
$transfer_method = 'queue';
$transfer_value = 'agent_' . $agent->ID; // User-specific queue name
}
if ($transfer_method) {
$formatted_agents[] = [
'id' => $agent->ID,
'name' => $agent->display_name,
'email' => $agent->user_email,
'status' => $agent->status ?: 'offline',
'is_available' => ($agent->status === 'available' && !$agent->current_call_sid),
'has_phone' => !empty($agent->phone_number),
'transfer_method' => $transfer_method,
'transfer_value' => $transfer_value
];
}
}
wp_send_json_success($formatted_agents);
}
/**
* AJAX handler for transferring call to agent queue
*/
public function ajax_transfer_to_agent_queue() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$agent_id = intval($_POST['agent_id']);
$transfer_method = sanitize_text_field($_POST['transfer_method']);
$transfer_value = sanitize_text_field($_POST['transfer_value']);
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
$twiml = new \Twilio\TwiML\VoiceResponse();
if ($transfer_method === 'phone') {
// Direct phone transfer
$twiml->say('Transferring your call. Please hold.');
$twiml->dial($transfer_value);
} else {
// Queue-based transfer for web phone agents
$queue_name = 'agent_' . $agent_id;
// Create or ensure the agent-specific queue exists in Twilio
$this->ensure_agent_queue_exists($queue_name, $agent_id);
// Notify the agent they have an incoming transfer
$this->notify_agent_of_transfer($agent_id, $call_sid);
$twiml->say('Transferring you to an agent. Please hold.');
$enqueue = $twiml->enqueue($queue_name);
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
}
// Update the call with the transfer TwiML
$call = $client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
wp_send_json_success(['message' => 'Call transferred successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
}
/**
* Ensure agent-specific queue exists
*/
private function ensure_agent_queue_exists($queue_name, $agent_id) {
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
// Check if queue exists
$queue = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $queues_table WHERE queue_name = %s",
$queue_name
));
if (!$queue) {
// Create the queue
$user = get_user_by('id', $agent_id);
$wpdb->insert($queues_table, [
'queue_name' => $queue_name,
'max_size' => 10,
'timeout_seconds' => 300,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
]);
}
}
/**
* Notify agent of incoming transfer
*/
private function notify_agent_of_transfer($agent_id, $call_sid) {
// Store notification in database or send real-time notification
// This could be enhanced with WebSockets or Server-Sent Events
// For now, just log it
error_log("TWP: Notifying agent $agent_id of incoming transfer for call $call_sid");
// You could also update the agent's status
global $wpdb;
$status_table = $wpdb->prefix . 'twp_agent_status';
$wpdb->update(
$status_table,
['current_call_sid' => $call_sid],
['user_id' => $agent_id]
);
}
/**
* AJAX handler for checking personal queue
*/
public function ajax_check_personal_queue() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$user_id = get_current_user_id();
$queue_name = 'agent_' . $user_id;
global $wpdb;
$queues_table = $wpdb->prefix . 'twp_call_queues';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Check if there are calls in the personal queue
$waiting_call = $wpdb->get_row($wpdb->prepare("
SELECT qc.*, q.id as queue_id
FROM $calls_table qc
JOIN $queues_table q ON qc.queue_id = q.id
WHERE q.queue_name = %s
AND qc.status = 'waiting'
ORDER BY COALESCE(qc.enqueued_at, qc.joined_at) ASC
LIMIT 1
", $queue_name));
if ($waiting_call) {
wp_send_json_success([
'has_waiting_call' => true,
'call_sid' => $waiting_call->call_sid,
'queue_id' => $waiting_call->queue_id,
'from_number' => $waiting_call->from_number,
'wait_time' => time() - strtotime($waiting_call->enqueued_at)
]);
} else {
wp_send_json_success(['has_waiting_call' => false]);
}
}
/**
* AJAX handler for accepting transfer call
*/
public function ajax_accept_transfer_call() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$queue_id = intval($_POST['queue_id']);
$user_id = get_current_user_id();
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Connect the call to the browser phone
$call = $client->calls($call_sid)->update([
'url' => home_url('/wp-json/twilio-webhook/v1/browser-voice'),
'method' => 'POST'
]);
// Update database to mark call as connected
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
$wpdb->update(
$calls_table,
[
'status' => 'connected',
'agent_id' => $user_id
],
['call_sid' => $call_sid]
);
// Update agent status
$status_table = $wpdb->prefix . 'twp_agent_status';
$wpdb->update(
$status_table,
['current_call_sid' => $call_sid],
['user_id' => $user_id]
);
wp_send_json_success(['message' => 'Transfer accepted']);
} catch (Exception $e) {
wp_send_json_error('Failed to accept transfer: ' . $e->getMessage());
}
}
}