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') )); // Voice token for VoIP register_rest_route('twilio-mobile/v1', '/voice/token', array( 'methods' => 'GET', 'callback' => array($this, 'get_voice_token'), 'permission_callback' => array($this->auth, 'verify_token') )); // Phone numbers for caller ID register_rest_route('twilio-mobile/v1', '/phone-numbers', array( 'methods' => 'GET', 'callback' => array($this, 'get_phone_numbers'), 'permission_callback' => array($this->auth, 'verify_token') )); // Outbound call (click-to-call via server) register_rest_route('twilio-mobile/v1', '/calls/outbound', array( 'methods' => 'POST', 'callback' => array($this, 'initiate_outbound_call'), 'permission_callback' => array($this->auth, 'verify_token') )); // FCM push credential setup (admin only) register_rest_route('twilio-mobile/v1', '/admin/push-credential', array( 'methods' => 'POST', 'callback' => array($this, 'setup_push_credential'), '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)); } require_once plugin_dir_path(__FILE__) . 'class-twp-agent-manager.php'; require_once plugin_dir_path(__FILE__) . 'class-twp-user-queue-manager.php'; // Handle login status change first (matches browser phone behavior) if ($is_logged_in !== null) { TWP_Agent_Manager::set_agent_login_status($user_id, (bool)$is_logged_in); } // Set agent status (handles auto_busy_at and all status fields) TWP_Agent_Manager::set_agent_status($user_id, $new_status); 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'; $groups_table = $wpdb->prefix . 'twp_group_members'; // Auto-create personal queues if they don't exist $extensions_table = $wpdb->prefix . 'twp_user_extensions'; $existing_extension = $wpdb->get_row($wpdb->prepare( "SELECT extension FROM $extensions_table WHERE user_id = %d", $user_id )); if (!$existing_extension) { require_once plugin_dir_path(__FILE__) . 'class-twp-user-queue-manager.php'; TWP_User_Queue_Manager::create_user_queues($user_id); } // Get queues where user is a member of the assigned agent group OR personal/hold queues $queues = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT q.id, q.queue_name, q.queue_type, q.extension, COUNT(c.id) as waiting_count FROM $queues_table q LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id LEFT JOIN $calls_table c ON q.id = c.queue_id AND c.status = 'waiting' WHERE (gm.user_id = %d AND gm.is_active = 1) OR (q.user_id = %d AND q.queue_type IN ('personal', 'hold')) GROUP BY q.id ORDER BY CASE WHEN q.queue_type = 'personal' THEN 1 WHEN q.queue_type = 'hold' THEN 2 ELSE 3 END, q.queue_name ASC ", $user_id, $user_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']; // Check for WebRTC client_identity parameter $body = $request->get_json_params(); $client_identity = isset($body['client_identity']) ? sanitize_text_field($body['client_identity']) : null; // 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 { if (!empty($client_identity)) { // WebRTC path: redirect the queued call to the Twilio Client device // Use the original caller's number as caller ID so it shows on the agent's device $caller_id = $call->from_number; if (empty($caller_id)) { $caller_id = $call->to_number; } if (empty($caller_id)) { $caller_id = get_option('twp_caller_id_number', ''); } $twiml = '' . htmlspecialchars($client_identity) . ''; error_log('TWP accept_call: call_sid=' . $call_sid . ' client=' . $client_identity . ' twiml=' . $twiml); $result = $twilio->update_call($call_sid, array('twiml' => $twiml)); error_log('TWP accept_call result: ' . json_encode($result)); if (!$result['success']) { return new WP_Error('twilio_error', $result['error'] ?? 'Failed to update call', array('status' => 500)); } // Update call record $wpdb->update( $calls_table, array( 'status' => 'connecting', 'agent_phone' => 'client:' . $client_identity, ), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); // Save current status before setting busy, so we can revert after call ends $status_table = $wpdb->prefix . 'twp_agent_status'; $current = $wpdb->get_row($wpdb->prepare( "SELECT status FROM $status_table WHERE user_id = %d", $user_id )); $pre_call_status = ($current && $current->status !== 'busy') ? $current->status : null; $wpdb->update( $status_table, array( 'status' => 'busy', 'current_call_sid' => $call_sid, 'pre_call_status' => $pre_call_status, 'auto_busy_at' => null, ), array('user_id' => $user_id), array('%s', '%s', '%s', '%s'), array('%d') ); // Cancel queue alert notifications on all agents' devices require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-fcm.php'; $fcm = new TWP_FCM(); $fcm->cancel_queue_alert_for_queue($call->queue_id, $call_sid); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call accepted via WebRTC client', 'call_sid' => $call_sid ), 200); } else { // Phone-based path (original flow): dial the agent's 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 and no client_identity provided', array('status' => 400)); } // 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) { $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)); } // Build identity for this agent $user = get_userdata($user_id); $clean_name = preg_replace('/[^a-zA-Z0-9]/', '', $user->user_login); if (empty($clean_name)) { $clean_name = 'user'; } $identity = 'agent' . $user_id . $clean_name; // Redirect customer back to agent's client $twiml = new \Twilio\TwiML\VoiceResponse(); $dial = $twiml->dial(); $dial->client($identity); $twilio->update_call($customer_call_sid, array('twiml' => $twiml->asXML())); return new WP_REST_Response(array( 'success' => true, 'message' => 'Call resumed from hold' ), 200); } catch (Exception $e) { return new WP_Error('unhold_error', $e->getMessage(), array('status' => 500)); } } /** * 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); } /** * Get Voice access token for VoIP */ public function get_voice_token($request) { $user_id = $this->auth->get_current_user_id(); $user = get_userdata($user_id); $clean_name = preg_replace('/[^a-zA-Z0-9]/', '', $user->user_login); if (empty($clean_name)) { $clean_name = 'user'; } $identity = 'agent' . $user_id . $clean_name; try { // Ensure Twilio SDK autoloader is loaded require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; new TWP_Twilio_API(); $account_sid = get_option('twp_twilio_account_sid'); $auth_token = get_option('twp_twilio_auth_token'); $twiml_app_sid = get_option('twp_twiml_app_sid'); if (empty($account_sid) || empty($auth_token) || empty($twiml_app_sid)) { return new WP_Error('token_error', 'Twilio credentials not configured', array('status' => 500)); } // AccessToken requires an API Key (not account credentials). // Auto-create and cache one if it doesn't exist yet. $api_key_sid = get_option('twp_twilio_api_key_sid'); $api_key_secret = get_option('twp_twilio_api_key_secret'); if (empty($api_key_sid) || empty($api_key_secret)) { $client = new \Twilio\Rest\Client($account_sid, $auth_token); $newKey = $client->newKeys->create(['friendlyName' => 'TWP Mobile Voice']); $api_key_sid = $newKey->sid; $api_key_secret = $newKey->secret; update_option('twp_twilio_api_key_sid', $api_key_sid); update_option('twp_twilio_api_key_secret', $api_key_secret); } $token = new \Twilio\Jwt\AccessToken($account_sid, $api_key_sid, $api_key_secret, 3600, $identity); $voiceGrant = new \Twilio\Jwt\Grants\VoiceGrant(); $voiceGrant->setOutgoingApplicationSid($twiml_app_sid); $voiceGrant->setIncomingAllow(true); // Include FCM push credential for incoming call notifications. // Auto-create from the stored Firebase service account JSON if not yet created. $push_credential_sid = get_option('twp_twilio_push_credential_sid'); if (empty($push_credential_sid)) { $push_credential_sid = $this->ensure_push_credential($account_sid, $auth_token); } if (!empty($push_credential_sid)) { $voiceGrant->setPushCredentialSid($push_credential_sid); } $token->addGrant($voiceGrant); return new WP_REST_Response(array( 'token' => $token->toJWT(), 'identity' => $identity, 'expires_in' => 3600 ), 200); } catch (Exception $e) { return new WP_Error('token_error', $e->getMessage(), array('status' => 500)); } } /** * Get available Twilio phone numbers for caller ID */ public function get_phone_numbers($request) { try { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; $twilio = new TWP_Twilio_API(); $result = $twilio->get_phone_numbers(); if (!$result['success']) { return new WP_Error('twilio_error', $result['error'], array('status' => 500)); } $phone_numbers = array(); foreach ($result['data']['incoming_phone_numbers'] as $number) { $phone_numbers[] = array( 'phone_number' => $number['phone_number'], 'friendly_name' => $number['friendly_name'], ); } return new WP_REST_Response(array( 'success' => true, 'phone_numbers' => $phone_numbers ), 200); } catch (Exception $e) { return new WP_Error('twilio_error', $e->getMessage(), array('status' => 500)); } } /** * 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; } /** * Admin endpoint to force re-creation of the Twilio Push Credential. */ public function setup_push_credential($request) { $user_id = $this->auth->get_current_user_id(); $user = get_userdata($user_id); if (!user_can($user, 'manage_options')) { return new WP_Error('forbidden', 'Admin access required', array('status' => 403)); } try { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php'; new TWP_Twilio_API(); $account_sid = get_option('twp_twilio_account_sid'); $auth_token = get_option('twp_twilio_auth_token'); // Force re-creation by clearing existing SID delete_option('twp_twilio_push_credential_sid'); $sid = $this->ensure_push_credential($account_sid, $auth_token); if (empty($sid)) { return new WP_Error('credential_error', 'Failed to create push credential. Check that Firebase service account JSON is configured in Mobile App Settings.', array('status' => 500)); } return new WP_REST_Response(array( 'success' => true, 'credential_sid' => $sid, ), 200); } catch (Exception $e) { error_log('TWP setup_push_credential error: ' . $e->getMessage()); return new WP_Error('credential_error', $e->getMessage(), array('status' => 500)); } } /** * Auto-create Twilio Push Credential from the stored Firebase service account JSON. * Returns the credential SID or empty string on failure. */ private function ensure_push_credential($account_sid, $auth_token) { $sa_json = get_option('twp_fcm_service_account_json', ''); if (empty($sa_json)) { return ''; } $sa = json_decode($sa_json, true); if (!$sa || empty($sa['project_id']) || empty($sa['private_key'])) { error_log('TWP: Firebase service account JSON is invalid'); return ''; } try { $client = new \Twilio\Rest\Client($account_sid, $auth_token); $credential = $client->notify->v1->credentials->create( 'fcm', [ 'friendlyName' => 'TWP Mobile FCM', 'secret' => $sa_json, ] ); update_option('twp_twilio_push_credential_sid', $credential->sid); error_log('TWP: Created Twilio push credential: ' . $credential->sid); return $credential->sid; } catch (Exception $e) { error_log('TWP ensure_push_credential error: ' . $e->getMessage()); return ''; } } /** * 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); } }