Files
twilio-wp-plugin/includes/class-twp-core.php
jknapp 90cb03acfd Fix workflow forward task immediate disconnection issue
Fixed critical bug where forward tasks in workflows would immediately disconnect calls instead of forwarding them properly.

Changes:
- Fixed append_twiml_element function to properly handle Dial elements with child Number elements
- Enhanced create_forward_twiml to extract numbers from nested data structures
- Added comprehensive error handling for missing forward numbers
- Added detailed logging throughout workflow execution for debugging
- Set default timeout of 30 seconds for forward operations

The issue was caused by the Dial element being converted to string which lost all child Number elements, resulting in an empty dial that would immediately disconnect.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 16:27:51 -07:00

447 lines
21 KiB
PHP

<?php
/**
* Core plugin class
*/
class TWP_Core {
protected $loader;
protected $plugin_name;
protected $version;
/**
* Constructor
*/
public function __construct() {
$this->version = TWP_VERSION;
$this->plugin_name = 'twilio-wp-plugin';
$this->load_dependencies();
$this->set_locale();
$this->ensure_user_roles();
$this->define_admin_hooks();
$this->define_public_hooks();
$this->define_api_hooks();
}
/**
* Load required dependencies
*/
private function load_dependencies() {
// Loader class
require_once TWP_PLUGIN_DIR . 'includes/class-twp-loader.php';
// API classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-twilio-api.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-elevenlabs-api.php';
// Feature classes
require_once TWP_PLUGIN_DIR . 'includes/class-twp-scheduler.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-queue.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-workflow.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-webhooks.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-call-logger.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-groups.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-agent-manager.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-callback-manager.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-user-queue-manager.php';
require_once TWP_PLUGIN_DIR . 'includes/class-twp-shortcodes.php';
// Admin classes
require_once TWP_PLUGIN_DIR . 'admin/class-twp-admin.php';
$this->loader = new TWP_Loader();
}
/**
* Define locale for internationalization
*/
private function set_locale() {
add_action('plugins_loaded', function() {
load_plugin_textdomain(
'twilio-wp-plugin',
false,
dirname(TWP_PLUGIN_BASENAME) . '/languages/'
);
});
}
/**
* Ensure custom user roles exist
*/
private function ensure_user_roles() {
$role = get_role('phone_agent');
if (!$role) {
// Create the Phone Agent role with limited capabilities
add_role('phone_agent', 'Phone Agent', array(
// Basic WordPress capabilities
'read' => true,
// Profile management
'edit_profile' => true,
// Phone agent specific capabilities
'twp_access_voicemails' => true,
'twp_access_call_log' => true,
'twp_access_agent_queue' => true,
'twp_access_outbound_calls' => true,
'twp_access_sms_inbox' => true,
'twp_access_browser_phone' => true,
'twp_access_phone_numbers' => true,
));
} else {
// Update existing role with any missing capabilities
$capabilities = array(
'twp_access_voicemails',
'twp_access_call_log',
'twp_access_agent_queue',
'twp_access_outbound_calls',
'twp_access_sms_inbox',
'twp_access_browser_phone',
'twp_access_phone_numbers'
);
foreach ($capabilities as $cap) {
if (!$role->has_cap($cap)) {
$role->add_cap($cap);
}
}
}
}
/**
* Register admin hooks
*/
private function define_admin_hooks() {
$plugin_admin = new TWP_Admin($this->get_plugin_name(), $this->get_version());
$this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles');
$this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts');
$this->loader->add_action('admin_menu', $plugin_admin, 'add_plugin_admin_menu');
$this->loader->add_action('admin_init', $plugin_admin, 'register_settings');
$this->loader->add_action('admin_notices', $plugin_admin, 'show_admin_notices');
// AJAX handlers
$this->loader->add_action('wp_ajax_twp_save_schedule', $plugin_admin, 'ajax_save_schedule');
$this->loader->add_action('wp_ajax_twp_delete_schedule', $plugin_admin, 'ajax_delete_schedule');
$this->loader->add_action('wp_ajax_twp_get_schedules', $plugin_admin, 'ajax_get_schedules');
$this->loader->add_action('wp_ajax_twp_get_schedule', $plugin_admin, 'ajax_get_schedule');
$this->loader->add_action('wp_ajax_twp_save_workflow', $plugin_admin, 'ajax_save_workflow');
$this->loader->add_action('wp_ajax_twp_update_workflow', $plugin_admin, 'ajax_save_workflow');
$this->loader->add_action('wp_ajax_twp_get_workflow', $plugin_admin, 'ajax_get_workflow');
$this->loader->add_action('wp_ajax_twp_get_workflow_phone_numbers', $plugin_admin, 'ajax_get_workflow_phone_numbers');
$this->loader->add_action('wp_ajax_twp_delete_workflow', $plugin_admin, 'ajax_delete_workflow');
// Phone number management AJAX
$this->loader->add_action('wp_ajax_twp_get_phone_numbers', $plugin_admin, 'ajax_get_phone_numbers');
$this->loader->add_action('wp_ajax_twp_search_available_numbers', $plugin_admin, 'ajax_search_available_numbers');
$this->loader->add_action('wp_ajax_twp_purchase_number', $plugin_admin, 'ajax_purchase_number');
$this->loader->add_action('wp_ajax_twp_configure_number', $plugin_admin, 'ajax_configure_number');
$this->loader->add_action('wp_ajax_twp_release_number', $plugin_admin, 'ajax_release_number');
// Queue management AJAX
$this->loader->add_action('wp_ajax_twp_get_queue', $plugin_admin, 'ajax_get_queue');
$this->loader->add_action('wp_ajax_twp_save_queue', $plugin_admin, 'ajax_save_queue');
$this->loader->add_action('wp_ajax_twp_get_queue_details', $plugin_admin, 'ajax_get_queue_details');
$this->loader->add_action('wp_ajax_twp_get_all_queues', $plugin_admin, 'ajax_get_all_queues');
$this->loader->add_action('wp_ajax_twp_delete_queue', $plugin_admin, 'ajax_delete_queue');
$this->loader->add_action('wp_ajax_twp_get_dashboard_stats', $plugin_admin, 'ajax_get_dashboard_stats');
// Queue management actions
$this->loader->add_action('wp_ajax_twp_get_queue_calls', $plugin_admin, 'ajax_get_queue_calls');
$this->loader->add_action('wp_ajax_twp_toggle_agent_login', $plugin_admin, 'ajax_toggle_agent_login');
$this->loader->add_action('wp_ajax_twp_answer_queue_call', $plugin_admin, 'ajax_answer_queue_call');
$this->loader->add_action('wp_ajax_twp_monitor_call', $plugin_admin, 'ajax_monitor_call');
$this->loader->add_action('wp_ajax_twp_toggle_call_recording', $plugin_admin, 'ajax_toggle_call_recording');
$this->loader->add_action('wp_ajax_twp_transfer_call', $plugin_admin, 'ajax_transfer_call');
$this->loader->add_action('wp_ajax_twp_send_to_voicemail', $plugin_admin, 'ajax_send_to_voicemail');
$this->loader->add_action('wp_ajax_twp_disconnect_call', $plugin_admin, 'ajax_disconnect_call');
$this->loader->add_action('wp_ajax_twp_get_transfer_targets', $plugin_admin, 'ajax_get_transfer_targets');
$this->loader->add_action('wp_ajax_twp_initialize_user_queues', $plugin_admin, 'ajax_initialize_user_queues');
// Eleven Labs AJAX
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_refresh_elevenlabs_voices', $plugin_admin, 'ajax_refresh_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');
// Voicemail management AJAX
$this->loader->add_action('wp_ajax_twp_get_voicemail', $plugin_admin, 'ajax_get_voicemail');
$this->loader->add_action('wp_ajax_twp_delete_voicemail', $plugin_admin, 'ajax_delete_voicemail');
$this->loader->add_action('wp_ajax_twp_transcribe_voicemail', $plugin_admin, 'ajax_transcribe_voicemail');
$this->loader->add_action('wp_ajax_twp_get_voicemail_audio', $plugin_admin, 'ajax_get_voicemail_audio');
$this->loader->add_action('wp_ajax_twp_get_user_voicemails', $plugin_admin, 'ajax_get_user_voicemails');
// Agent group management AJAX
$this->loader->add_action('wp_ajax_twp_get_all_groups', $plugin_admin, 'ajax_get_all_groups');
$this->loader->add_action('wp_ajax_twp_get_group', $plugin_admin, 'ajax_get_group');
$this->loader->add_action('wp_ajax_twp_save_group', $plugin_admin, 'ajax_save_group');
$this->loader->add_action('wp_ajax_twp_delete_group', $plugin_admin, 'ajax_delete_group');
$this->loader->add_action('wp_ajax_twp_get_group_members', $plugin_admin, 'ajax_get_group_members');
$this->loader->add_action('wp_ajax_twp_add_group_member', $plugin_admin, 'ajax_add_group_member');
$this->loader->add_action('wp_ajax_twp_remove_group_member', $plugin_admin, 'ajax_remove_group_member');
// Agent queue management AJAX
$this->loader->add_action('wp_ajax_twp_accept_call', $plugin_admin, 'ajax_accept_call');
// Discord/Slack notification system
$this->loader->add_action('init', $this, 'setup_notification_cron');
$this->loader->add_action('twp_check_queue_timeouts', $this, 'check_queue_timeouts');
$this->loader->add_action('wp_ajax_twp_accept_next_queue_call', $plugin_admin, 'ajax_accept_next_queue_call');
$this->loader->add_action('wp_ajax_twp_get_waiting_calls', $plugin_admin, 'ajax_get_waiting_calls');
$this->loader->add_action('wp_ajax_twp_get_agent_queues', $plugin_admin, 'ajax_get_agent_queues');
$this->loader->add_action('wp_ajax_twp_get_requeue_queues', $plugin_admin, 'ajax_get_requeue_queues');
$this->loader->add_action('wp_ajax_twp_set_agent_status', $plugin_admin, 'ajax_set_agent_status');
$this->loader->add_action('wp_ajax_twp_get_call_details', $plugin_admin, 'ajax_get_call_details');
// Callback and outbound call AJAX
$this->loader->add_action('wp_ajax_twp_request_callback', $plugin_admin, 'ajax_request_callback');
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call', $plugin_admin, 'ajax_initiate_outbound_call');
$this->loader->add_action('wp_ajax_twp_initiate_outbound_call_with_from', $plugin_admin, 'ajax_initiate_outbound_call_with_from');
$this->loader->add_action('wp_ajax_twp_get_callbacks', $plugin_admin, 'ajax_get_callbacks');
// Phone number maintenance
$this->loader->add_action('wp_ajax_twp_update_phone_status_callbacks', $plugin_admin, 'ajax_update_phone_status_callbacks');
$this->loader->add_action('wp_ajax_twp_toggle_number_status_callback', $plugin_admin, 'ajax_toggle_number_status_callback');
// Browser phone
$this->loader->add_action('wp_ajax_twp_generate_capability_token', $plugin_admin, 'ajax_generate_capability_token');
$this->loader->add_action('wp_ajax_twp_save_call_mode', $plugin_admin, 'ajax_save_call_mode');
$this->loader->add_action('wp_ajax_twp_auto_configure_twiml_app', $plugin_admin, 'ajax_auto_configure_twiml_app');
$this->loader->add_action('wp_ajax_twp_configure_phone_numbers_only', $plugin_admin, 'ajax_configure_phone_numbers_only');
// SMS management
$this->loader->add_action('wp_ajax_twp_delete_sms', $plugin_admin, 'ajax_delete_sms');
$this->loader->add_action('wp_ajax_twp_delete_conversation', $plugin_admin, 'ajax_delete_conversation');
$this->loader->add_action('wp_ajax_twp_get_conversation', $plugin_admin, 'ajax_get_conversation');
$this->loader->add_action('wp_ajax_twp_send_sms_reply', $plugin_admin, 'ajax_send_sms_reply');
// Call control actions
$this->loader->add_action('wp_ajax_twp_toggle_hold', $plugin_admin, 'ajax_toggle_hold');
$this->loader->add_action('wp_ajax_twp_transfer_call', $plugin_admin, 'ajax_transfer_call');
$this->loader->add_action('wp_ajax_twp_get_transfer_agents', $plugin_admin, 'ajax_get_transfer_agents');
$this->loader->add_action('wp_ajax_twp_requeue_call', $plugin_admin, 'ajax_requeue_call');
$this->loader->add_action('wp_ajax_twp_start_recording', $plugin_admin, 'ajax_start_recording');
$this->loader->add_action('wp_ajax_twp_stop_recording', $plugin_admin, 'ajax_stop_recording');
$this->loader->add_action('wp_ajax_twp_get_call_recordings', $plugin_admin, 'ajax_get_call_recordings');
$this->loader->add_action('wp_ajax_twp_delete_recording', $plugin_admin, 'ajax_delete_recording');
$this->loader->add_action('wp_ajax_twp_get_online_agents', $plugin_admin, 'ajax_get_online_agents');
$this->loader->add_action('wp_ajax_twp_transfer_to_agent_queue', $plugin_admin, 'ajax_transfer_to_agent_queue');
$this->loader->add_action('wp_ajax_twp_check_personal_queue', $plugin_admin, 'ajax_check_personal_queue');
$this->loader->add_action('wp_ajax_twp_accept_transfer_call', $plugin_admin, 'ajax_accept_transfer_call');
// Frontend browser phone AJAX handlers are already covered by the admin handlers above
// since they check permissions internally
}
/**
* Register public hooks
*/
private function define_public_hooks() {
// Webhook endpoints
$webhooks = new TWP_Webhooks();
$this->loader->add_action('init', $webhooks, 'register_endpoints');
// Initialize Agent Manager
TWP_Agent_Manager::init();
// Initialize Shortcodes
TWP_Shortcodes::init();
// Scheduled events
$scheduler = new TWP_Scheduler();
$this->loader->add_action('twp_check_schedules', $scheduler, 'check_active_schedules');
$queue = new TWP_Call_Queue();
$this->loader->add_action('twp_process_queue', $queue, 'process_waiting_calls');
// Callback processing
$this->loader->add_action('twp_process_callbacks', 'TWP_Callback_Manager', 'process_callbacks');
// Call queue cleanup
$this->loader->add_action('twp_cleanup_old_calls', $this, 'cleanup_old_calls');
// Schedule cron events
if (!wp_next_scheduled('twp_check_schedules')) {
wp_schedule_event(time(), 'twp_every_minute', 'twp_check_schedules');
}
if (!wp_next_scheduled('twp_process_queue')) {
wp_schedule_event(time(), 'twp_every_30_seconds', 'twp_process_queue');
}
if (!wp_next_scheduled('twp_process_callbacks')) {
wp_schedule_event(time(), 'twp_every_minute', 'twp_process_callbacks');
}
if (!wp_next_scheduled('twp_cleanup_old_calls')) {
wp_schedule_event(time(), 'hourly', 'twp_cleanup_old_calls');
}
}
/**
* Register API hooks
*/
private function define_api_hooks() {
// REST API endpoints
add_action('rest_api_init', function() {
register_rest_route('twilio-wp/v1', '/schedules', array(
'methods' => 'GET',
'callback' => array('TWP_Scheduler', 'get_schedules'),
'permission_callback' => function() {
return current_user_can('manage_options');
}
));
register_rest_route('twilio-wp/v1', '/workflows', array(
'methods' => 'GET',
'callback' => array('TWP_Workflow', 'get_workflows'),
'permission_callback' => function() {
return current_user_can('manage_options');
}
));
register_rest_route('twilio-wp/v1', '/queue-status', array(
'methods' => 'GET',
'callback' => array('TWP_Call_Queue', 'get_queue_status'),
'permission_callback' => function() {
return current_user_can('manage_options');
}
));
});
}
/**
* Run the loader
*/
public function run() {
// Initialize webhooks
$webhooks = new TWP_Webhooks();
$webhooks->register_endpoints();
// Add custom cron schedules
add_filter('cron_schedules', function($schedules) {
$schedules['twp_every_minute'] = array(
'interval' => 60,
'display' => __('Every Minute', 'twilio-wp-plugin')
);
$schedules['twp_every_30_seconds'] = array(
'interval' => 30,
'display' => __('Every 30 Seconds', 'twilio-wp-plugin')
);
return $schedules;
});
$this->loader->run();
}
/**
* Get plugin name
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* Get version
*/
public function get_version() {
return $this->version;
}
/**
* Cleanup old stuck calls
*/
public function cleanup_old_calls() {
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// Clean up calls that have been in 'answered' status for more than 2 hours
// These are likely stuck due to missed webhooks or other issues
$updated = $wpdb->query(
"UPDATE $calls_table
SET status = 'completed', ended_at = NOW()
WHERE status = 'answered'
AND joined_at < DATE_SUB(NOW(), INTERVAL 2 HOUR)"
);
if ($updated > 0) {
error_log("TWP Cleanup: Updated {$updated} stuck calls from 'answered' to 'completed' status");
}
// Backup check for waiting calls (status callbacks should handle most cases)
// Only check older calls that might have missed status callbacks
$waiting_calls = $wpdb->get_results(
"SELECT call_sid FROM $calls_table
WHERE status = 'waiting'
AND joined_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
AND joined_at > DATE_SUB(NOW(), INTERVAL 6 HOUR)
LIMIT 5"
);
if (!empty($waiting_calls)) {
$twilio = new TWP_Twilio_API();
$cleaned_count = 0;
foreach ($waiting_calls as $call) {
try {
// Check call status with Twilio
$call_info = $twilio->get_call_info($call->call_sid);
if ($call_info && isset($call_info['status'])) {
// If call is completed, busy, failed, or canceled, remove from queue
if (in_array($call_info['status'], ['completed', 'busy', 'failed', 'canceled'])) {
$wpdb->update(
$calls_table,
array(
'status' => 'hangup',
'ended_at' => current_time('mysql')
),
array('call_sid' => $call->call_sid),
array('%s', '%s'),
array('%s')
);
$cleaned_count++;
error_log("TWP Cleanup: Detected ended call {$call->call_sid} with status {$call_info['status']}");
}
}
} catch (Exception $e) {
error_log("TWP Cleanup: Error checking call {$call->call_sid}: " . $e->getMessage());
}
}
if ($cleaned_count > 0) {
error_log("TWP Cleanup: Cleaned up {$cleaned_count} ended calls from queue");
}
}
// Clean up very old waiting calls (older than 24 hours) - these are likely orphaned
$updated_waiting = $wpdb->query(
"UPDATE $calls_table
SET status = 'timeout', ended_at = NOW()
WHERE status = 'waiting'
AND joined_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"
);
if ($updated_waiting > 0) {
error_log("TWP Cleanup: Updated {$updated_waiting} old waiting calls to 'timeout' status");
}
}
/**
* Setup notification cron job
*/
public function setup_notification_cron() {
if (!wp_next_scheduled('twp_check_queue_timeouts')) {
wp_schedule_event(time(), 'twp_every_minute', 'twp_check_queue_timeouts');
}
}
/**
* Check for queue timeouts and send notifications
*/
public function check_queue_timeouts() {
require_once dirname(__FILE__) . '/class-twp-notifications.php';
TWP_Notifications::check_queue_timeouts();
}
}