api_url = AGENTORIE_CHAT_API_URL; $this->license_key = agentorie_chat_get_setting( 'license_key', '' ); } /** * Get language code mapping (short code to full locale). * * @since 1.0.0 * @return array Mapping of short language codes to full locale codes. */ private static function get_lang_map() { return array( 'fr' => 'fr-FR', 'en' => 'en-US', 'es' => 'es-ES', 'de' => 'de-DE', 'it' => 'it-IT', 'pt' => 'pt-PT', 'ru' => 'ru-RU', 'zh' => 'zh-CN', 'ja' => 'ja-JP', 'ar' => 'ar-SA', ); } /** * Send message to SaaS. * * @param string $session_id Session ID. * @param string $message Message content. * @param array $context Context data. * @param bool $exclude_from_stats Exclude from stats. * @return array|WP_Error Response or error. */ public function send_message( $session_id, $message, $context = array(), $exclude_from_stats = false ) { $endpoint = $this->api_url . '/chat'; $body = array( 'session_id' => $session_id, 'message' => $message, 'exclude_from_stats' => $exclude_from_stats, 'site_url' => home_url(), 'context' => array_merge( array( 'visitor_id' => isset( $context['visitor_id'] ) ? $context['visitor_id'] : '', 'current_url' => isset( $context['current_url'] ) ? $context['current_url'] : '', 'page_title' => isset( $context['page_title'] ) ? $context['page_title'] : '', 'referrer' => isset( $context['referrer'] ) ? $context['referrer'] : '', 'user_agent' => isset( $context['user_agent'] ) ? $context['user_agent'] : '', 'ip_address' => Agentorie_Session::get_client_ip(), 'language' => isset( $context['language'] ) ? $context['language'] : get_locale(), ), $context ), ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Sync config with SaaS. * * @param array $config Configuration data. * @return array|WP_Error Response or error. */ public function sync_config( $config = array() ) { $endpoint = $this->api_url . '/license/config'; $body = array_merge( array( 'site_url' => home_url() ), $config ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Complete onboarding. * * @return array|WP_Error Response or error. */ public function complete_onboarding() { $endpoint = $this->api_url . '/onboarding/complete'; return $this->make_request( 'POST', $endpoint, array() ); } /** * Notify SaaS that indexed content has been updated. * * This triggers cache-busting by incrementing the content_version * on the SaaS side. The next chat request will use a different * query parameter, bypassing any cached responses. * * @since 1.0.0 * @return array|WP_Error Response or error. */ public function notify_content_updated() { $endpoint = $this->api_url . '/content/updated'; $body = array( 'event' => 'content.updated', 'timestamp' => time(), ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Check license status. * * @param string $license_key License key. * @param bool $force_refresh Force refresh. * @return array|WP_Error Response or error. */ public static function check_license( $license_key, $force_refresh = false ) { $instance = self::get_instance(); if ( ! $force_refresh ) { $cached = get_transient( 'agentorie_chat_license_cache' ); if ( false !== $cached ) { return $cached; } } $endpoint = $instance->api_url . '/license/status'; $body = array( 'license_key' => $license_key, 'site_url' => home_url(), ); $response = $instance->make_request( 'POST', $endpoint, $body ); if ( ! is_wp_error( $response ) ) { set_transient( 'agentorie_chat_license_cache', $response, 6 * HOUR_IN_SECONDS ); self::process_license_response( $response ); } return $response; } /** * Process license response and update settings. * * @param array $response License response data. */ private static function process_license_response( $response ) { if ( ! isset( $response['status'] ) ) { return; } $status = ( 'active' === $response['status'] ) ? 'active' : 'inactive'; agentorie_chat_update_setting( 'license_status', $status ); if ( isset( $response['features'] ) && is_array( $response['features'] ) ) { agentorie_chat_update_setting( 'license_features', $response['features'] ); } if ( isset( $response['active_languages'] ) && is_array( $response['active_languages'] ) ) { agentorie_chat_update_setting( 'active_languages', array_map( 'sanitize_text_field', $response['active_languages'] ) ); } if ( isset( $response['rate_limit'] ) ) { agentorie_chat_update_setting( 'rate_limit', (int) $response['rate_limit'] ); } if ( isset( $response['demos'] ) && is_array( $response['demos'] ) ) { $demos = $response['demos']; agentorie_chat_update_setting( 'demo_voice_active', isset( $demos['voice_active'] ) ? (bool) $demos['voice_active'] : false ); agentorie_chat_update_setting( 'demo_voice_remaining', isset( $demos['voice_remaining'] ) ? (int) $demos['voice_remaining'] : 0 ); agentorie_chat_update_setting( 'demo_lang_remaining', isset( $demos['lang_remaining'] ) ? (int) $demos['lang_remaining'] : 0 ); agentorie_chat_update_setting( 'demo_lang_active', isset( $demos['lang_active'] ) ? sanitize_text_field( $demos['lang_active'] ) : '' ); } if ( isset( $response['usage'] ) && is_array( $response['usage'] ) ) { if ( isset( $response['usage']['test_conversations'] ) ) { agentorie_chat_update_setting( 'test_conversations_count', (int) $response['usage']['test_conversations'] ); } } if ( isset( $response['config']['lead_collection_instructions'] ) ) { agentorie_chat_update_setting( 'lead_collection_instructions', sanitize_textarea_field( $response['config']['lead_collection_instructions'] ) ); } if ( isset( $response['config']['lead_collection_email'] ) ) { agentorie_chat_update_setting( 'lead_collection_email', sanitize_email( $response['config']['lead_collection_email'] ) ); } if ( isset( $response['config']['voice_enabled'] ) ) { agentorie_chat_update_setting( 'voice_enabled', (bool) $response['config']['voice_enabled'] ); } if ( isset( $response['config']['content_indexing_limit'] ) ) { agentorie_chat_update_setting( 'content_indexing_limit', (int) $response['config']['content_indexing_limit'] ); } if ( isset( $response['config']['woo_indexing_limit'] ) ) { agentorie_chat_update_setting( 'woo_indexing_limit', (int) $response['config']['woo_indexing_limit'] ); } if ( isset( $response['config']['language'] ) ) { // SaaS now stores BCP-47 directly (fr-FR, en-US, etc.). $saas_lang = sanitize_text_field( $response['config']['language'] ); // Only set language from SaaS on initial activation (when no local value exists yet). if ( ! agentorie_chat_get_setting( 'default_language' ) ) { agentorie_chat_update_setting( 'default_language', $saas_lang ); } } if ( 'active' !== $status ) { agentorie_chat_update_setting( 'enabled', false ); } } /** * Invalidate license cache. */ public static function invalidate_license_cache() { delete_transient( 'agentorie_chat_license_cache' ); } /** * Activate license. * * @param string $license_key License key. * @return array|WP_Error Response or error. */ public static function activate_license( $license_key ) { $instance = self::get_instance(); $endpoint = $instance->api_url . '/license/activate'; $body = array( 'license_key' => $license_key, 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), 'site_url' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'admin_email' => get_option( 'admin_email' ), 'wp_version' => get_bloginfo( 'version' ), 'php_version' => PHP_VERSION, 'plugin_version' => AGENTORIE_CHAT_VERSION, ); $response = $instance->make_request( 'POST', $endpoint, $body, false ); if ( ! is_wp_error( $response ) && ! empty( $response['success'] ) ) { $instance->license_key = $license_key; delete_transient( 'agentorie_chat_license_cache' ); } return $response; } /** * Deactivate license. * * @param string $license_key License key. * @return array|WP_Error Response or error. */ public static function deactivate_license( $license_key = '' ) { $instance = self::get_instance(); $endpoint = $instance->api_url . '/license/deactivate'; $body = array( 'license_key' => $license_key ? $license_key : $instance->license_key, 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), 'site_url' => home_url(), ); return $instance->make_request( 'POST', $endpoint, $body ); } /** * Send admin message (stub for future WebSocket integration). * * @param string $session_id Session ID. * @param string $message Message content. * @param int $admin_id Admin user ID. * @return array Empty array (handled via WebSocket). */ public function send_admin_message( $session_id, $message, $admin_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Stub method, parameters required by interface. return array(); } /** * Takeover conversation (stub for future WebSocket integration). * * @param string $session_id Session ID. * @param int $admin_id Admin user ID. * @return array Empty array (handled via WebSocket). */ public function takeover_conversation( $session_id, $admin_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Stub method, parameters required by interface. return array(); } /** * Release conversation (stub for future WebSocket integration). * * @param string $session_id Session ID. * @param int $admin_id Admin user ID. * @return array Empty array (handled via WebSocket). */ public function release_conversation( $session_id, $admin_id ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Stub method, parameters required by interface. return array(); } /** * Updates the presence status of an admin on livechat. * * @param int $admin_id WordPress user ID. * @param string $admin_name Display name. * @param bool $online Online status. * @return array|WP_Error Response array or WP_Error on failure. */ public function update_presence( int $admin_id, string $admin_name, bool $online ) { $endpoint = $this->api_url . '/livechat/presence'; $body = array( 'admin_id' => (string) $admin_id, 'admin_name' => $admin_name, 'online' => $online, ); $response = $this->make_request( 'POST', $endpoint, $body ); // If WP_Error (e.g. 403), extract data from body. if ( is_wp_error( $response ) ) { $error_data = $response->get_error_data(); // Data is in error_data['body'] (see make_request). if ( isset( $error_data['body'] ) && is_array( $error_data['body'] ) ) { $body_data = $error_data['body']; $error_code = isset( $body_data['error_code'] ) ? $body_data['error_code'] : null; // If it's a limit error, create WP_Error with proper data. if ( 'admin_limit_reached' === $error_code ) { return new WP_Error( 'agentorie_chat_admin_limit_reached', isset( $body_data['error'] ) ? $body_data['error'] : 'Admin seat limit reached', array( 'error_code' => 'admin_limit_reached', 'online_count' => isset( $body_data['online_count'] ) ? $body_data['online_count'] : 0, 'max_admins' => isset( $body_data['max_admins'] ) ? $body_data['max_admins'] : 1, ) ); } } return $response; } // Check if it's a limit error in response (success=false case). if ( isset( $response['success'] ) && false === $response['success'] ) { return new WP_Error( isset( $response['error_code'] ) ? $response['error_code'] : 'agentorie_chat_presence_error', isset( $response['error'] ) ? $response['error'] : 'Unknown error', array( 'error_code' => isset( $response['error_code'] ) ? $response['error_code'] : null, 'online_count' => isset( $response['online_count'] ) ? $response['online_count'] : 0, 'max_admins' => isset( $response['max_admins'] ) ? $response['max_admins'] : 1, ) ); } return $response; } /** * Speech to text. * * @param string $audio_data Base64 audio data. * @param string $language Language code. * @return array|WP_Error Response or error. */ public function speech_to_text( $audio_data, $language = 'fr-FR' ) { $endpoint = $this->api_url . '/voice/stt'; $body = array( 'audio' => $audio_data, 'language' => $language, 'site_url' => home_url(), ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Text to speech. * * @param string $text Text content. * @param string $language Language code. * @return array|WP_Error Response or error. */ public function text_to_speech( $text, $language = 'fr-FR' ) { $endpoint = $this->api_url . '/voice/tts'; $body = array( 'text' => $text, 'language' => $language, 'site_url' => home_url(), ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Translate widget UI texts via SaaS (Gemini + Redis cache). * * @since 1.1.6 * @param string $target_lang BCP-47 language code. * @param array $texts Key-value pairs of texts to translate. * @return array|WP_Error Response with translations or error. */ public function translate_texts( $target_lang, $texts ) { $endpoint = $this->api_url . '/translate'; $body = array( 'target_lang' => $target_lang, 'texts' => $texts, ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Check if API is reachable. * * @return bool True if reachable. */ public function is_api_reachable() { $response = wp_remote_get( $this->api_url . '/health', array( 'timeout' => 5, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { return false; } $code = wp_remote_retrieve_response_code( $response ); return $code >= 200 && $code < 300; } /** * Request WebSocket token from SaaS. * * @param string $session_id Session ID. * @param string $user_type User type (visitor or admin). * @param bool $test_mode Whether the visitor is in test mode (excluded from stats). * @return array|WP_Error Response or error. */ public function make_ws_token_request( $session_id, $user_type = 'visitor', $test_mode = false ) { $endpoint = $this->api_url . '/ws/token'; $body = array( 'session_id' => $session_id, 'user_type' => $user_type, 'test_mode' => (bool) $test_mode, ); return $this->make_request( 'POST', $endpoint, $body ); } /** * Handle search content REST endpoint. * * @param WP_REST_Request $request The request object. * @return WP_REST_Response Response. */ public function handle_search_content( $request ) { $query = sanitize_text_field( $request->get_param( 'query' ) ); $limit = absint( $request->get_param( 'limit' ) ); if ( empty( $query ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => 'Query parameter is required', ), 400 ); } if ( $limit < 1 || $limit > 20 ) { $limit = 5; } if ( ! class_exists( 'Agentorie_Content_Indexer' ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => 'Content Indexer not available', ), 500 ); } $indexer = Agentorie_Content_Indexer::get_instance(); $results = $indexer->search( $query, $limit ); return new WP_REST_Response( array( 'success' => true, 'results' => $results, 'query' => $query, ), 200 ); } /** * Handle get indexed content REST endpoint. * * Returns indexed content for direct AI injection. Supports an optional * limit parameter to restrict the number of results, allowing the SaaS * to enforce plan-based quotas. * * @since 1.0.0 * @param WP_REST_Request $request The request object. * @return WP_REST_Response Response with indexed content. */ public function handle_get_indexed_content( $request ) { if ( ! class_exists( 'Agentorie_Content_Indexer' ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => 'Content Indexer not available', ), 500 ); } $indexer = Agentorie_Content_Indexer::get_instance(); // Get optional limit parameter from SaaS (plan-based quota). $limit = $request->get_param( 'limit' ); $limit = ( null !== $limit ) ? absint( $limit ) : null; // Get results with optional limit. $results = $indexer->get_all_indexed( $limit ); // Get total count for UI display (shows "X indexed, Y used by AI"). $total_indexed = $indexer->get_total_count(); return new WP_REST_Response( array( 'success' => true, 'results' => $results, 'count' => count( $results ), 'total_indexed' => $total_indexed, ), 200 ); } /** * Handle get indexed products REST endpoint. * * Returns WooCommerce products with pre-stored enriched data (price, stock, * categories, attributes) from the index. Returns empty results if no * products are indexed. * * @since 1.2.0 * @param WP_REST_Request $request The request object. * @return WP_REST_Response Response with indexed products. */ public function handle_get_indexed_products( $request ) { if ( ! class_exists( 'Agentorie_Content_Indexer' ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => 'Content Indexer not available', ), 500 ); } $indexer = Agentorie_Content_Indexer::get_instance(); // Get optional limit parameter from SaaS (plan-based quota). $limit = $request->get_param( 'limit' ); $limit = ( null !== $limit ) ? absint( $limit ) : null; $results = $indexer->get_all_indexed_products( $limit ); $product_stats = $indexer->get_product_stats(); return new WP_REST_Response( array( 'success' => true, 'results' => $results, 'count' => count( $results ), 'total_indexed' => $product_stats['total_indexed'], ), 200 ); } /** * Make HTTP request to SaaS API. * * @param string $method HTTP method. * @param string $endpoint API endpoint URL. * @param array $body Request body. * @param bool $include_auth Include authentication headers. * @return array|WP_Error Response data or error. */ private function make_request( $method, $endpoint, $body = array(), $include_auth = true ) { $headers = array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'User-Agent' => 'Agentorie-Chat-Plugin/' . AGENTORIE_CHAT_VERSION, ); if ( $include_auth ) { $license_key = isset( $body['license_key'] ) ? $body['license_key'] : $this->license_key; if ( ! empty( $license_key ) ) { $headers['X-License-Key'] = $license_key; } $headers['X-Domain'] = wp_parse_url( home_url(), PHP_URL_HOST ); } $args = array( 'method' => $method, 'headers' => $headers, 'timeout' => $this->timeout, 'sslverify' => true, ); if ( ! empty( $body ) && in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) ) { $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( $endpoint, $args ); if ( is_wp_error( $response ) ) { Agentorie_Logger::error( 'API request failed', array( 'endpoint' => $endpoint, 'error' => $response->get_error_message(), ) ); return $response; } $response_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); $data = json_decode( $response_body, true ); if ( $response_code >= 400 ) { $error_message = isset( $data['error'] ) ? $data['error'] : ( isset( $data['message'] ) ? $data['message'] : 'Unknown API error' ); Agentorie_Logger::error( 'API error response', array( 'endpoint' => $endpoint, 'code' => $response_code, 'message' => $error_message, ) ); return new WP_Error( 'agentorie_chat_api_error', $error_message, array( 'status' => $response_code, 'body' => $data, ) ); } if ( null === $data && ! empty( $response_body ) ) { Agentorie_Logger::warning( 'Invalid JSON response from API', array( 'endpoint' => $endpoint ) ); return new WP_Error( 'agentorie_chat_json_error', __( 'Invalid response from API', 'agentorie-chat' ) ); } return $data ? $data : array(); } }