Twilio Phone Settings

Your phone number for receiving forwarded calls (include country code)

ID); ?>

Your availability for receiving calls

Phone number already in use by ' . esc_html($duplicate_user->display_name) . '

'; }); } else { update_user_meta($user_id, 'twp_phone_number', $validation_result['formatted']); } } else { add_action('admin_notices', function() use ($validation_result) { echo '

Phone number error: ' . esc_html($validation_result['error']) . '

'; }); } } else { update_user_meta($user_id, 'twp_phone_number', ''); } } // Save agent status if (isset($_POST['twp_agent_status'])) { self::set_agent_status_with_notification($user_id, sanitize_text_field($_POST['twp_agent_status'])); } } /** * Set agent status */ public static function set_agent_status($user_id, $status, $call_sid = null) { global $wpdb; $table_name = $wpdb->prefix . 'twp_agent_status'; $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); if ($existing) { return $wpdb->update( $table_name, array( 'status' => $status, 'current_call_sid' => $call_sid, 'last_activity' => current_time('mysql') ), array('user_id' => $user_id), array('%s', '%s', '%s'), array('%d') ); } else { return $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'status' => $status, 'current_call_sid' => $call_sid, 'last_activity' => current_time('mysql') ), array('%d', '%s', '%s', '%s') ); } } /** * Get agent status */ public static function get_agent_status($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'twp_agent_status'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); } /** * Get available agents */ public static function get_available_agents($group_id = null) { global $wpdb; $status_table = $wpdb->prefix . 'twp_agent_status'; $users_table = $wpdb->prefix . 'users'; $usermeta_table = $wpdb->prefix . 'usermeta'; if ($group_id) { // Get available agents from a specific group $members_table = $wpdb->prefix . 'twp_group_members'; $query = $wpdb->prepare(" SELECT u.ID as user_id, u.display_name, um.meta_value as phone_number, s.status, gm.priority FROM $members_table gm JOIN $users_table u ON gm.user_id = u.ID LEFT JOIN $status_table s ON u.ID = s.user_id LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number' WHERE gm.group_id = %d AND gm.is_active = 1 AND (s.status = 'available' OR s.status IS NULL) AND um.meta_value IS NOT NULL AND um.meta_value != '' ORDER BY gm.priority ASC, u.display_name ASC ", $group_id); } else { // Get all available agents $query = " SELECT u.ID as user_id, u.display_name, um.meta_value as phone_number, s.status FROM $users_table u LEFT JOIN $status_table s ON u.ID = s.user_id LEFT JOIN $usermeta_table um ON u.ID = um.user_id AND um.meta_key = 'twp_phone_number' WHERE (s.status = 'available' OR s.status IS NULL) AND um.meta_value IS NOT NULL AND um.meta_value != '' ORDER BY u.display_name ASC "; } return $wpdb->get_results($query); } /** * Accept a queued call */ public static function accept_queued_call($call_id, $user_id) { global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; // Get the call details $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $calls_table WHERE id = %d AND status = 'waiting'", $call_id )); if (!$call) { return array('success' => false, 'error' => 'Call not found or already answered'); } // Check user's call mode $call_mode = get_user_meta($user_id, 'twp_call_mode', true); if (empty($call_mode)) { $call_mode = 'cell'; // Default to cell phone } // Get user's phone number (needed for cell mode and as fallback) $phone_number = get_user_meta($user_id, 'twp_phone_number', true); if ($call_mode === 'cell' && !$phone_number) { return array('success' => false, 'error' => 'No phone number configured for user'); } // Update call status $wpdb->update( $calls_table, array( 'status' => 'answered', 'answered_at' => current_time('mysql') ), array('id' => $call_id), array('%s', '%s'), array('%d') ); // Set agent status to busy self::set_agent_status($user_id, 'busy', $call->call_sid); // Make a new call to the agent with proper caller ID $twilio = new TWP_Twilio_API(); // Get the queue's notification number for proper caller ID (same logic as SMS webhook) $queues_table = $wpdb->prefix . 'twp_call_queues'; $queue_info = $wpdb->get_row($wpdb->prepare( "SELECT notification_number FROM $queues_table WHERE id = %d", $call->queue_id )); // Priority: 1) Queue's notification number, 2) Call's original to_number, 3) Default SMS number $workflow_number = null; if (!empty($queue_info->notification_number)) { $workflow_number = $queue_info->notification_number; error_log('TWP Web Accept: Using queue notification number: ' . $workflow_number); } elseif (!empty($call->to_number)) { $workflow_number = $call->to_number; error_log('TWP Web Accept: Using original workflow number: ' . $workflow_number); } else { $workflow_number = TWP_Twilio_API::get_sms_from_number(); error_log('TWP Web Accept: Using default number: ' . $workflow_number); } // Create webhook URL for screening the agent call $connect_url = home_url('/wp-json/twilio-webhook/v1/agent-screen'); $connect_url = add_query_arg(array( 'queued_call_id' => $call_id, 'customer_number' => $call->from_number, 'customer_call_sid' => $call->call_sid ), $connect_url); // Create status callback URL to detect voicemail/no-answer $status_callback_url = home_url('/wp-json/twilio-webhook/v1/agent-call-status'); $status_callback_url = add_query_arg(array( 'queued_call_id' => $call_id, 'user_id' => $user_id, 'original_call_sid' => $call->call_sid ), $status_callback_url); // Handle different call modes if ($call_mode === 'browser') { // For browser mode, redirect the existing call to the browser client $current_user = get_userdata($user_id); // Twilio requires alphanumeric characters only - must match generate_capability_token $clean_name = preg_replace('/[^a-zA-Z0-9]/', '', $current_user->display_name); if (empty($clean_name)) { $clean_name = 'user'; } $client_name = 'agent' . $user_id . $clean_name; // Create TwiML to redirect call to browser client $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Connecting you to an agent.', ['voice' => 'alice']); $dial = $twiml->dial(); $dial->setAttribute('timeout', 30); $dial->client($client_name); // Update the existing call to redirect to browser $result = $twilio->update_call($call->call_sid, [ 'twiml' => $twiml->asXML() ]); } else { // For cell mode, make call to agent's phone number $result = $twilio->make_call( $phone_number, $connect_url, $status_callback_url, // Track call status for voicemail detection $workflow_number // Use queue's phone number as caller ID ); } if ($result['success']) { // Log the call acceptance TWP_Call_Logger::log_call(array( 'call_sid' => $call->call_sid, 'from_number' => $call->from_number, 'to_number' => $phone_number, 'status' => 'agent_answered', 'workflow_name' => 'Queue: Agent Accept', 'actions_taken' => json_encode(array( 'agent_id' => $user_id, 'agent_name' => get_userdata($user_id)->display_name, 'queue_id' => $call->queue_id )) )); return array('success' => true); } else { return array('success' => false, 'error' => 'Failed to forward call'); } } /** * Handle call status callback */ public static function handle_call_status($call_sid, $status) { global $wpdb; $status_table = $wpdb->prefix . 'twp_agent_status'; // If call completed, set agent back to available if ($status === 'completed') { $agent = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $status_table WHERE current_call_sid = %s", $call_sid )); if ($agent) { self::set_agent_status($agent->user_id, 'available', null); } } } /** * Initiate simultaneous ring to group members */ public static function ring_group($group_id, $call_data) { $members = TWP_Agent_Groups::get_group_phone_numbers($group_id); if (empty($members)) { return false; } $twilio = new TWP_Twilio_API(); $twiml = new \Twilio\TwiML\VoiceResponse(); // Play a message while dialing $twiml->say('Please wait while we connect your call...', ['voice' => 'alice']); // Create a dial with simultaneous ring to all group members $dial = $twiml->dial([ 'timeout' => 30, 'action' => home_url('/wp-json/twilio-webhook/v1/dial-status'), 'method' => 'POST' ]); // Add each member's number to the dial for simultaneous ring foreach ($members as $member) { if ($member['phone_number']) { $dial->number($member['phone_number']); } } // If no one answers, go to voicemail $twiml->say('All agents are currently unavailable. Please leave a message after the beep.', ['voice' => 'alice']); $twiml->record([ 'maxLength' => 120, 'transcribe' => true, 'transcribeCallback' => home_url('/wp-json/twilio-webhook/v1/transcription') ]); return $twiml->asXML(); } /** * Get agent dashboard stats */ public static function get_agent_stats($user_id) { global $wpdb; $log_table = $wpdb->prefix . 'twp_call_log'; // Get today's stats $today = date('Y-m-d'); $stats = array( 'calls_today' => $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $log_table WHERE actions_taken LIKE %s AND DATE(created_at) = %s", '%"agent_id":' . $user_id . '%', $today )), 'total_calls' => $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $log_table WHERE actions_taken LIKE %s", '%"agent_id":' . $user_id . '%' )), 'avg_duration' => $wpdb->get_var($wpdb->prepare( "SELECT AVG(duration) FROM $log_table WHERE actions_taken LIKE %s AND duration > 0", '%"agent_id":' . $user_id . '%' )) ); return $stats; } /** * Send SMS notification to agent when they become available */ public static function notify_agent_availability($user_id, $status) { $phone_number = get_user_meta($user_id, 'twp_phone_number', true); $sms_number = get_option('twp_sms_notification_number'); if (empty($phone_number) || empty($sms_number)) { return false; } if ($status === 'available') { // Check for waiting calls immediately global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $waiting_count = $wpdb->get_var("SELECT COUNT(*) FROM $calls_table WHERE status = 'waiting'"); if ($waiting_count > 0) { $message = "You are now available. There are {$waiting_count} calls waiting. Text '1' to receive the next call."; } else { $message = "You are now available for calls. You'll receive notifications when calls are waiting."; } $twilio = new TWP_Twilio_API(); // Get SMS from number with proper priority (no workflow context here) $from_number = TWP_Twilio_API::get_sms_from_number(); return $twilio->send_sms($phone_number, $message, $from_number); } return true; } /** * Check if agent can receive calls (has phone number and is available) */ public static function can_agent_receive_calls($user_id) { $phone_number = get_user_meta($user_id, 'twp_phone_number', true); $status = self::get_agent_status($user_id); return !empty($phone_number) && $status && $status->status === 'available'; } /** * Get agents by group who can receive calls */ public static function get_available_group_agents($group_id) { $group_members = TWP_Agent_Groups::get_group_members($group_id); $available_agents = array(); foreach ($group_members as $member) { if (self::can_agent_receive_calls($member->user_id)) { $phone_number = get_user_meta($member->user_id, 'twp_phone_number', true); if ($phone_number) { $available_agents[] = array( 'user_id' => $member->user_id, 'phone_number' => $phone_number, 'priority' => $member->priority ); } } } // Sort by priority (lower numbers = higher priority) usort($available_agents, function($a, $b) { return $a['priority'] - $b['priority']; }); return $available_agents; } /** * Enhanced set agent status with SMS notifications */ public static function set_agent_status_with_notification($user_id, $status, $call_sid = null) { $old_status = self::get_agent_status($user_id); $result = self::set_agent_status($user_id, $status, $call_sid); // Send SMS notification if status changed to available if ($result && $status === 'available' && (!$old_status || $old_status->status !== 'available')) { self::notify_agent_availability($user_id, $status); } return $result; } /** * Validate phone number format */ public static function validate_phone_number($phone_number) { $phone = trim($phone_number); // Remove any non-numeric characters except + and spaces $cleaned = preg_replace('/[^0-9+\s\-\(\)]/', '', $phone); // Check if it starts with + (international format) if (strpos($cleaned, '+') === 0) { $formatted = preg_replace('/[^0-9+]/', '', $cleaned); // Must be at least 10 digits after the + if (strlen($formatted) >= 11 && strlen($formatted) <= 16) { return array( 'valid' => true, 'formatted' => $formatted ); } else { return array( 'valid' => false, 'error' => 'Phone number must be 10-15 digits with country code (e.g., +1234567890)' ); } } else { // Check if it's a US number without country code $digits = preg_replace('/[^0-9]/', '', $cleaned); if (strlen($digits) === 10) { // Assume US number, add +1 return array( 'valid' => true, 'formatted' => '+1' . $digits ); } else if (strlen($digits) === 11 && substr($digits, 0, 1) === '1') { // US number with 1 prefix return array( 'valid' => true, 'formatted' => '+' . $digits ); } else { return array( 'valid' => false, 'error' => 'Invalid phone number format. Use +1234567890 or 1234567890 format' ); } } } /** * Check if phone number is already in use by another agent */ public static function is_phone_number_duplicate($phone_number, $exclude_user_id = null) { $users = get_users(array( 'meta_key' => 'twp_phone_number', 'meta_value' => $phone_number, 'meta_compare' => '=' )); foreach ($users as $user) { if ($exclude_user_id && $user->ID == $exclude_user_id) { continue; } return $user; } return false; } }