auth = new TWP_Mobile_Auth(); } /** * Register REST API endpoints */ public function register_endpoints() { add_action('rest_api_init', function() { // Agent status endpoints register_rest_route('twilio-mobile/v1', '/agent/status', array( 'methods' => 'GET', 'callback' => array($this, 'get_agent_status'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/agent/status', array( 'methods' => 'POST', 'callback' => array($this, 'update_agent_status'), 'permission_callback' => array($this->auth, 'verify_token') )); // Queue state endpoint register_rest_route('twilio-mobile/v1', '/queues/state', array( 'methods' => 'GET', 'callback' => array($this, 'get_queue_state'), 'permission_callback' => array($this->auth, 'verify_token') )); // Queue calls (specific queue) register_rest_route('twilio-mobile/v1', '/queues/(?P\d+)/calls', array( 'methods' => 'GET', 'callback' => array($this, 'get_queue_calls'), 'permission_callback' => array($this->auth, 'verify_token') )); // Call control endpoints register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/accept', array( 'methods' => 'POST', 'callback' => array($this, 'accept_call'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/reject', array( 'methods' => 'POST', 'callback' => array($this, 'reject_call'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/hold', array( 'methods' => 'POST', 'callback' => array($this, 'hold_call'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/unhold', array( 'methods' => 'POST', 'callback' => array($this, 'unhold_call'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/calls/(?P[^/]+)/transfer', array( 'methods' => 'POST', 'callback' => array($this, 'transfer_call'), 'permission_callback' => array($this->auth, 'verify_token') )); // FCM token registration register_rest_route('twilio-mobile/v1', '/fcm/register', array( 'methods' => 'POST', 'callback' => array($this, 'register_fcm_token'), 'permission_callback' => array($this->auth, 'verify_token') )); // Agent phone number register_rest_route('twilio-mobile/v1', '/agent/phone', array( 'methods' => 'GET', 'callback' => array($this, 'get_agent_phone'), 'permission_callback' => array($this->auth, 'verify_token') )); register_rest_route('twilio-mobile/v1', '/agent/phone', array( 'methods' => 'POST', 'callback' => array($this, 'update_agent_phone'), 'permission_callback' => array($this->auth, 'verify_token') )); }); } /** * Get agent status */ public function get_agent_status($request) { $user_id = $this->auth->get_current_user_id(); global $wpdb; $table = $wpdb->prefix . 'twp_agent_status'; $status = $wpdb->get_row($wpdb->prepare( "SELECT status, is_logged_in, current_call_sid, last_activity, available_for_queues FROM $table WHERE user_id = %d", $user_id )); if (!$status) { // Create default status $wpdb->insert( $table, array('user_id' => $user_id, 'status' => 'offline', 'is_logged_in' => 0), array('%d', '%s', '%d') ); $status = (object) array( 'status' => 'offline', 'is_logged_in' => 0, 'current_call_sid' => null, 'last_activity' => current_time('mysql'), 'available_for_queues' => 1 ); } return new WP_REST_Response(array( 'success' => true, 'status' => $status->status, 'is_logged_in' => (bool)$status->is_logged_in, 'current_call_sid' => $status->current_call_sid, 'last_activity' => $status->last_activity, 'available_for_queues' => (bool)$status->available_for_queues ), 200); } /** * Update agent status */ public function update_agent_status($request) { $user_id = $this->auth->get_current_user_id(); $new_status = $request->get_param('status'); $is_logged_in = $request->get_param('is_logged_in'); if (!in_array($new_status, array('available', 'busy', 'offline'))) { return new WP_Error('invalid_status', 'Status must be available, busy, or offline', array('status' => 400)); } global $wpdb; $table = $wpdb->prefix . 'twp_agent_status'; // Check if status exists $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE user_id = %d", $user_id )); $data = array( 'status' => $new_status, 'last_activity' => current_time('mysql') ); if ($is_logged_in !== null) { $data['is_logged_in'] = $is_logged_in ? 1 : 0; if ($is_logged_in) { $data['logged_in_at'] = current_time('mysql'); } } if ($exists) { $wpdb->update( $table, $data, array('user_id' => $user_id), array('%s', '%s'), array('%d') ); } else { $data['user_id'] = $user_id; $wpdb->insert($table, $data); } return new WP_REST_Response(array( 'success' => true, 'message' => 'Status updated successfully' ), 200); } /** * Get queue state (all queues user has access to) */ public function get_queue_state($request) { $user_id = $this->auth->get_current_user_id(); global $wpdb; $queues_table = $wpdb->prefix . 'twp_call_queues'; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $assignments_table = $wpdb->prefix . 'twp_queue_assignments'; // Get queues assigned to this user $queue_ids = $wpdb->get_col($wpdb->prepare( "SELECT queue_id FROM $assignments_table WHERE user_id = %d", $user_id )); // Also include personal queues $personal_queue_ids = $wpdb->get_col($wpdb->prepare( "SELECT id FROM $queues_table WHERE user_id = %d", $user_id )); $all_queue_ids = array_unique(array_merge($queue_ids, $personal_queue_ids)); if (empty($all_queue_ids)) { return new WP_REST_Response(array( 'success' => true, 'queues' => array() ), 200); } $queue_ids_str = implode(',', array_map('intval', $all_queue_ids)); // Get queue information with call counts $queues = $wpdb->get_results(" SELECT q.id, q.queue_name, q.queue_type, q.extension, COUNT(c.id) as waiting_count FROM $queues_table q LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting' WHERE q.id IN ($queue_ids_str) GROUP BY q.id "); $result = array(); foreach ($queues as $queue) { $result[] = array( 'id' => (int)$queue->id, 'name' => $queue->queue_name, 'type' => $queue->queue_type, 'extension' => $queue->extension, 'waiting_count' => (int)$queue->waiting_count ); } return new WP_REST_Response(array( 'success' => true, 'queues' => $result ), 200); } /** * Get calls in a specific queue */ public function get_queue_calls($request) { $user_id = $this->auth->get_current_user_id(); $queue_id = (int)$request['id']; // Verify user has access to this queue if (!$this->user_has_queue_access($user_id, $queue_id)) { return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403)); } global $wpdb; $table = $wpdb->prefix . 'twp_queued_calls'; $calls = $wpdb->get_results($wpdb->prepare( "SELECT call_sid, from_number, to_number, position, status, joined_at, enqueued_at FROM $table WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue_id )); $result = array(); foreach ($calls as $call) { $result[] = array( 'call_sid' => $call->call_sid, 'from_number' => $call->from_number, 'to_number' => $call->to_number, 'position' => (int)$call->position, 'status' => $call->status, 'wait_time' => $this->calculate_wait_time($call->enqueued_at ?: $call->joined_at) ); } return new WP_REST_Response(array( 'success' => true, 'calls' => $result ), 200); } /** * Accept a call (dequeue and connect to agent) */ public function accept_call($request) { $user_id = $this->auth->get_current_user_id(); $call_sid = $request['call_sid']; // Get agent phone number $agent_number = get_user_meta($user_id, 'twp_agent_phone', true); if (empty($agent_number)) { return new WP_Error('no_phone', 'No phone number configured for agent', array('status' => 400)); } // Initialize Twilio API require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $twilio = new TWP_Twilio_API(); // Get call info from queue global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'", $call_sid )); if (!$call) { return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404)); } // Verify user has access to this queue if (!$this->user_has_queue_access($user_id, $call->queue_id)) { return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403)); } try { // Connect agent to call $agent_call = $twilio->create_call( $agent_number, $call->to_number, array( 'url' => site_url('/wp-json/twilio-webhook/v1/connect-agent'), 'statusCallback' => site_url('/wp-json/twilio-webhook/v1/agent-call-status'), 'statusCallbackEvent' => array('completed', 'no-answer', 'busy', 'failed'), 'timeout' => 30 ) ); // Update call record $wpdb->update( $calls_table, array( 'status' => 'connecting', 'agent_phone' => $agent_number, 'agent_call_sid' => $agent_call->sid ), array('call_sid' => $call_sid), array('%s', '%s', '%s'), array('%s') ); // Update agent status $status_table = $wpdb->prefix . 'twp_agent_status'; $wpdb->update( $status_table, array('status' => 'busy', 'current_call_sid' => $call_sid), array('user_id' => $user_id), array('%s', '%s'), array('%d') ); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call accepted, connecting to agent', 'agent_call_sid' => $agent_call->sid ), 200); } catch (Exception $e) { return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500)); } } /** * Reject a call (send to voicemail) */ public function reject_call($request) { $user_id = $this->auth->get_current_user_id(); $call_sid = $request['call_sid']; global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $calls_table WHERE call_sid = %s AND status = 'waiting'", $call_sid )); if (!$call) { return new WP_Error('call_not_found', 'Call not found or no longer waiting', array('status' => 404)); } // Verify user has access to this queue if (!$this->user_has_queue_access($user_id, $call->queue_id)) { return new WP_Error('forbidden', 'You do not have access to this queue', array('status' => 403)); } try { // Initialize Twilio API require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $twilio = new TWP_Twilio_API(); // Redirect call to voicemail $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('The agent is unavailable. Please leave a message after the tone.'); $twiml->record(array( 'action' => site_url('/wp-json/twilio-webhook/v1/voicemail-complete'), 'maxLength' => 120, 'transcribe' => true )); $twiml->say('We did not receive a recording. Goodbye.'); $twilio->update_call($call_sid, array('twiml' => $twiml->asXML())); // Update call status $wpdb->update( $calls_table, array('status' => 'voicemail', 'ended_at' => current_time('mysql')), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call sent to voicemail' ), 200); } catch (Exception $e) { return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500)); } } /** * Hold a call */ public function hold_call($request) { $user_id = $this->auth->get_current_user_id(); $call_sid = $request['call_sid']; try { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION); $twilio = new TWP_Twilio_API(); // Find customer call leg $customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio); if (!$customer_call_sid) { return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404)); } // Get user's hold queue global $wpdb; $ext_table = $wpdb->prefix . 'twp_user_extensions'; $queues_table = $wpdb->prefix . 'twp_call_queues'; $extension = $wpdb->get_row($wpdb->prepare( "SELECT hold_queue_id FROM $ext_table WHERE user_id = %d", $user_id )); if (!$extension || !$extension->hold_queue_id) { return new WP_Error('no_hold_queue', 'No hold queue configured', array('status' => 400)); } $hold_queue = $wpdb->get_row($wpdb->prepare( "SELECT queue_name, wait_music_url FROM $queues_table WHERE id = %d", $extension->hold_queue_id )); // Put call on hold $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Please hold while we transfer your call.'); $enqueue = $twiml->enqueue($hold_queue->queue_name, array( 'waitUrl' => $hold_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait') )); $twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML())); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call placed on hold' ), 200); } catch (Exception $e) { return new WP_Error('hold_error', $e->getMessage(), array('status' => 500)); } } /** * Unhold a call (resume from hold queue) */ public function unhold_call($request) { // Implementation would retrieve from hold queue and reconnect return new WP_REST_Response(array( 'success' => true, 'message' => 'Unhold functionality - to be implemented with queue retrieval' ), 501); } /** * Transfer a call to another extension/queue */ public function transfer_call($request) { $user_id = $this->auth->get_current_user_id(); $call_sid = $request['call_sid']; $target = $request->get_param('target'); // Extension number or queue ID if (empty($target)) { return new WP_Error('missing_target', 'Transfer target is required', array('status' => 400)); } try { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-admin.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION); $twilio = new TWP_Twilio_API(); // Find customer call leg $customer_call_sid = $admin->find_customer_call_leg($call_sid, $twilio); if (!$customer_call_sid) { return new WP_Error('call_not_found', 'Could not find customer call leg', array('status' => 404)); } // Look up target (extension or queue) global $wpdb; $ext_table = $wpdb->prefix . 'twp_user_extensions'; $queues_table = $wpdb->prefix . 'twp_call_queues'; // Try as extension first $target_queue = $wpdb->get_row($wpdb->prepare( "SELECT q.* FROM $queues_table q JOIN $ext_table e ON q.id = e.personal_queue_id WHERE e.extension = %s", $target )); // If not extension, try as queue ID if (!$target_queue && is_numeric($target)) { $target_queue = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $queues_table WHERE id = %d", $target )); } if (!$target_queue) { return new WP_Error('invalid_target', 'Transfer target not found', array('status' => 404)); } // Transfer to queue $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Transferring your call.'); $twiml->enqueue($target_queue->queue_name, array( 'waitUrl' => $target_queue->wait_music_url ?: site_url('/wp-json/twilio-webhook/v1/queue-wait') )); $twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML())); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call transferred successfully' ), 200); } catch (Exception $e) { return new WP_Error('transfer_error', $e->getMessage(), array('status' => 500)); } } /** * Register FCM token for push notifications */ public function register_fcm_token($request) { $user_id = $this->auth->get_current_user_id(); $fcm_token = $request->get_param('fcm_token'); $refresh_token = $request->get_param('refresh_token'); if (empty($fcm_token)) { return new WP_Error('missing_token', 'FCM token is required', array('status' => 400)); } $this->auth->update_fcm_token($user_id, $refresh_token, $fcm_token); return new WP_REST_Response(array( 'success' => true, 'message' => 'FCM token registered successfully' ), 200); } /** * Get agent phone number */ public function get_agent_phone($request) { $user_id = $this->auth->get_current_user_id(); $agent_number = get_user_meta($user_id, 'twp_agent_phone', true); return new WP_REST_Response(array( 'success' => true, 'phone_number' => $agent_number ?: null ), 200); } /** * Update agent phone number */ public function update_agent_phone($request) { $user_id = $this->auth->get_current_user_id(); $phone_number = $request->get_param('phone_number'); if (empty($phone_number)) { return new WP_Error('missing_phone', 'Phone number is required', array('status' => 400)); } // Validate E.164 format if (!preg_match('/^\+[1-9]\d{1,14}$/', $phone_number)) { return new WP_Error('invalid_phone', 'Phone number must be in E.164 format (+1XXXXXXXXXX)', array('status' => 400)); } update_user_meta($user_id, 'twp_agent_phone', $phone_number); return new WP_REST_Response(array( 'success' => true, 'message' => 'Phone number updated successfully' ), 200); } /** * Check if user has access to a queue */ private function user_has_queue_access($user_id, $queue_id) { global $wpdb; $queues_table = $wpdb->prefix . 'twp_call_queues'; $assignments_table = $wpdb->prefix . 'twp_queue_assignments'; // Check if it's user's personal queue $is_personal = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $queues_table WHERE id = %d AND user_id = %d", $queue_id, $user_id )); if ($is_personal) { return true; } // Check if user is assigned to this queue $is_assigned = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $assignments_table WHERE queue_id = %d AND user_id = %d", $queue_id, $user_id )); return (bool)$is_assigned; } /** * Calculate wait time in seconds */ private function calculate_wait_time($start_time) { if (!$start_time) { return 0; } $start = strtotime($start_time); $now = current_time('timestamp'); return max(0, $now - $start); } }