options = $options; $this->logger = $logger; } /** * Updates the internal options array. Typically called after settings are saved. * * @param array $options The new plugin options array. */ public function set_options( $options ) { $this->options = $options; } // --- Public Methods (AI Task Selectors) --- /** * Rewrites content using the currently selected main AI provider (Gemini or Groq). * * @param string $original_text The original text to rewrite. * @return string|WP_Error The rewritten text on success, or a WP_Error object on failure. */ public function rewrite_content( $original_text ) { if ( isset( $this->options['main_ia_provider'] ) && 'groq' === $this->options['main_ia_provider'] ) { return $this->rewrite_with_groq( $original_text ); } // Default to Gemini if not Groq or if provider setting is missing/invalid. return $this->rewrite_with_gemini( $original_text ); } /** * Rewrites a title using the selected main AI provider. * * @param string $original_title The original title. * @param string $rewritten_content The rewritten content (used for context by the AI). * @return string|WP_Error The newly generated title on success, or a WP_Error object on failure. */ public function rewrite_title( $original_title, $rewritten_content ) { if ( isset( $this->options['main_ia_provider'] ) && 'groq' === $this->options['main_ia_provider'] ) { return $this->rewrite_title_with_groq( $original_title, $rewritten_content ); } return $this->rewrite_title_with_gemini( $original_title, $rewritten_content ); } /** * Categorizes content using the selected main AI provider. * * @param string $rewritten_content The content to categorize. * @return string|WP_Error The suggested category name on success, or a WP_Error object on failure. */ public function categorize_content( $rewritten_content ) { if ( isset( $this->options['main_ia_provider'] ) && 'groq' === $this->options['main_ia_provider'] ) { return $this->categorize_with_groq( $rewritten_content ); } return $this->categorize_with_gemini( $rewritten_content ); } /** * Generates tags for content using the selected main AI provider. * * @param string $rewritten_content The content to generate tags for. * @return array|WP_Error An array of suggested tag strings on success, or a WP_Error object on failure. */ public function generate_tags( $rewritten_content ) { if ( isset( $this->options['main_ia_provider'] ) && 'groq' === $this->options['main_ia_provider'] ) { return $this->generate_tags_with_groq( $rewritten_content ); } return $this->generate_tags_with_gemini( $rewritten_content ); } // --- Model Lists --- /** * Retrieves a list of all available Gemini models supported by the plugin. * * @return array An associative array where keys are model IDs and values contain label and API version. */ public function get_all_gemini_models() { return array( 'gemini-1.5-flash-001' => array( /* translators: Model name in dropdown */ 'label' => __( 'Gemini 1.5 Flash (Recommended)', 'ainp-ai-native-publisher' ), 'version' => 'v1beta', ), 'gemini-1.5-pro-001' => array( /* translators: Model name in dropdown */ 'label' => __( 'Gemini 1.5 Pro (Advanced)', 'ainp-ai-native-publisher' ), 'version' => 'v1beta', ), 'gemini-pro' => array( /* translators: Model name in dropdown */ 'label' => __( 'Gemini Pro (Legacy Stable)', 'ainp-ai-native-publisher' ), 'version' => 'v1beta', ), 'gemini-1.0-pro' => array( /* translators: Model name in dropdown */ 'label' => __( 'Gemini 1.0 Pro (Legacy)', 'ainp-ai-native-publisher' ), 'version' => 'v1beta', ), ); } /** * Retrieves a list of all available Groq models supported by the plugin. * * @return array An associative array where keys are model IDs and values are display labels. */ public function get_all_groq_models() { return array( /* translators: Model name in dropdown */ 'llama-3.1-8b-instant' => __( 'Llama 3.1 8b (Fast & Efficient)', 'ainp-ai-native-publisher' ), /* translators: Model name in dropdown */ 'llama-3.1-70b-versatile' => __( 'Llama 3.1 70b (Advanced)', 'ainp-ai-native-publisher' ), /* translators: Model name in dropdown */ 'gemma2-9b-it' => __( 'Gemma 2 9b (from Google)', 'ainp-ai-native-publisher' ), ); } // --- Connection Test Methods --- /** * Tests the connection to all configured Gemini models using the provided API key. */ public function test_gemini_connections() { $results = array(); if ( empty( $this->options['gemini_api_key'] ) ) { return array( 'error' => __( 'No Gemini API key has been entered.', 'ainp-ai-native-publisher' ) ); } $payload = array( 'contents' => array( array( 'parts' => array( array( 'text' => 'Respond only with the word "OK"' ) ) ) ) ); $all_models = $this->get_all_gemini_models(); foreach ( $all_models as $model_name => $model_info ) { $response = $this->call_gemini_api( $model_name, $model_info['version'], 'generateContent', $payload, true ); if ( is_wp_error( $response ) ) { $results[ $model_name ] = array( 'status' => 'error', 'message' => $response->get_error_message(), ); } elseif ( isset( $response['candidates'][0]['content']['parts'][0]['text'] ) && false !== stripos( $response['candidates'][0]['content']['parts'][0]['text'], 'OK' ) ) { $results[ $model_name ] = array( 'status' => 'success', 'message' => __( 'Connection successful.', 'ainp-ai-native-publisher' ), ); } else { $results[ $model_name ] = array( 'status' => 'error', 'message' => __( 'Unexpected response format.', 'ainp-ai-native-publisher' ), ); } } return $results; } /** * Tests the connection to the Groq API using the selected model and API key. */ public function test_groq_connection() { if ( empty( $this->options['groq_api_key'] ) ) { return array( 'error' => __( 'No Groq API key has been entered.', 'ainp-ai-native-publisher' ) ); } $selected_model = $this->options['groq_model_selection'] ?? 'llama-3.1-8b-instant'; $payload = array( 'messages' => array( array( 'role' => 'user', 'content' => 'Respond only with the word "OK"' ) ), 'model' => $selected_model, ); $response = $this->call_groq_api( $payload, true ); if ( is_wp_error( $response ) ) { return array( 'status' => 'error', 'message' => $response->get_error_message(), ); } elseif ( isset( $response['choices'][0]['message']['content'] ) && false !== stripos( $response['choices'][0]['message']['content'], 'OK' ) ) { return array( 'status' => 'success', 'message' => __( 'Connection successful.', 'ainp-ai-native-publisher' ), ); } else { return array( 'status' => 'error', 'message' => __( 'Unexpected response from the Groq API.', 'ainp-ai-native-publisher' ), ); } } /** * Tests the connection to the Unsplash API. */ public function test_unsplash_connection() { if ( empty( $this->options['unsplash_access_key'] ) ) { return array( 'error' => __( 'No Unsplash Access Key has been entered.', 'ainp-ai-native-publisher' ) ); } $response = $this->call_unsplash_api( 'nature', true ); if ( is_wp_error( $response ) ) { return array( 'status' => 'error', 'message' => $response->get_error_message(), ); } elseif ( isset( $response['results'] ) ) { return array( 'status' => 'success', 'message' => __( 'Connection successful.', 'ainp-ai-native-publisher' ), ); } else { return array( 'status' => 'error', 'message' => __( 'Unexpected response structure from Unsplash.', 'ainp-ai-native-publisher' ), ); } } // --- Helper: Language Instruction --- private function get_language_instruction() { $target_language = $this->options['target_language'] ?? 'Portuguese (Brazil)'; return "\n\nIMPORTANT: Write the final output strictly in " . $target_language . "."; } // --- Helper: Category List Expansion --- private function get_expanded_category_list() { // Get actual categories from WP $site_categories = get_categories( array( 'hide_empty' => 0, 'fields' => 'names' ) ); // Fallback list of standard topics to ensure AI has options even on empty sites $default_topics = array( 'News', 'Politics', 'Economy', 'Technology', 'Science', 'Health', 'Sports', 'Entertainment', 'World', 'Business' ); // Merge and unique $all_cats = array_unique( array_merge( $site_categories, $default_topics ) ); // Limit to 50 to prevent token overflow return implode( ', ', array_slice( $all_cats, 0, 50 ) ); } // --- Private Methods (AI-Specific Logic) --- private function rewrite_with_gemini( $original_text ) { $originality_prefix = ! empty( $this->options['prompt_originality_prefix'] ) ? trim( $this->options['prompt_originality_prefix'] ) . "\n\n---\n\n" : ''; $base_prompt = str_replace( '{original_text}', wp_strip_all_tags( $original_text ), $this->options['prompt_rewrite'] ); $prompt = $originality_prefix . $base_prompt . $this->get_language_instruction(); $payload = array( 'contents' => array( array( 'parts' => array( array( 'text' => $prompt ) ) ) ) ); $all_models = $this->get_all_gemini_models(); $model_name = $this->options['gemini_model_selection'] ?? 'gemini-1.5-flash-001'; $version = $all_models[ $model_name ]['version'] ?? 'v1beta'; $response = $this->call_gemini_api( $model_name, $version, 'generateContent', $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['candidates'][0]['content']['parts'][0]['text'] ) ) { // Security: Sanitize HTML content before returning return wp_kses_post( $this->format_ia_response_content( $response['candidates'][0]['content']['parts'][0]['text'] ) ); } return new WP_Error( 'gemini_rewrite_error', __( 'Unexpected response from Gemini API for content rewriting (missing text).', 'ainp-ai-native-publisher' ), $response ); } private function rewrite_with_groq( $original_text ) { $originality_prefix = ! empty( $this->options['prompt_originality_prefix'] ) ? trim( $this->options['prompt_originality_prefix'] ) . "\n\n---\n\n" : ''; $base_prompt = str_replace( '{original_text}', wp_strip_all_tags( $original_text ), $this->options['prompt_rewrite'] ); $prompt = $originality_prefix . $base_prompt . $this->get_language_instruction(); $payload = array( 'messages' => array( array( 'role' => 'user', 'content' => $prompt ) ), 'model' => $this->options['groq_model_selection'] ?? 'llama-3.1-8b-instant', 'temperature' => 0.7, ); $response = $this->call_groq_api( $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['choices'][0]['message']['content'] ) ) { // Security: Sanitize HTML content before returning return wp_kses_post( $this->format_ia_response_content( $response['choices'][0]['message']['content'] ) ); } return new WP_Error( 'groq_rewrite_error', __( 'Unexpected response from Groq API for content rewriting (missing text).', 'ainp-ai-native-publisher' ), $response ); } private function rewrite_title_with_gemini( $original_title, $rewritten_content ) { $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 500 ) . '...'; $base_prompt = str_replace( array( '{original_title}', '{rewritten_content}' ), array( wp_strip_all_tags( $original_title ), $prompt_context_content ), $this->options['prompt_title'] ); $prompt = $base_prompt . $this->get_language_instruction(); $payload = array( 'contents' => array( array( 'parts' => array( array( 'text' => $prompt ) ) ) ), 'generationConfig' => array( 'maxOutputTokens' => 50, 'temperature' => 0.7, ), ); $all_models = $this->get_all_gemini_models(); $model_name = $this->options['gemini_model_selection'] ?? 'gemini-1.5-flash-001'; $version = $all_models[ $model_name ]['version'] ?? 'v1beta'; $response = $this->call_gemini_api( $model_name, $version, 'generateContent', $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['candidates'][0]['content']['parts'][0]['text'] ) ) { return $this->format_ia_response_title( $response['candidates'][0]['content']['parts'][0]['text'], $original_title ); } /* translators: %s: Original post title */ $error_message = sprintf( __( 'Unexpected response from Gemini API for title of "%1$s".', 'ainp-ai-native-publisher' ), esc_html( $original_title ) ); return new WP_Error( 'gemini_title_unexpected_response', $error_message, $response ); } private function rewrite_title_with_groq( $original_title, $rewritten_content ) { $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 500 ) . '...'; $base_prompt = str_replace( array( '{original_title}', '{rewritten_content}' ), array( wp_strip_all_tags( $original_title ), $prompt_context_content ), $this->options['prompt_title'] ); $prompt = $base_prompt . $this->get_language_instruction(); $payload = array( 'messages' => array( array( 'role' => 'user', 'content' => $prompt ) ), 'model' => $this->options['groq_model_selection'] ?? 'llama-3.1-8b-instant', 'max_tokens' => 50, 'temperature' => 0.7, ); $response = $this->call_groq_api( $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['choices'][0]['message']['content'] ) ) { return $this->format_ia_response_title( $response['choices'][0]['message']['content'], $original_title ); } /* translators: %s: Original post title */ $error_message = sprintf( __( 'Unexpected response from Groq API for title of "%1$s".', 'ainp-ai-native-publisher' ), esc_html( $original_title ) ); return new WP_Error( 'groq_title_unexpected_response', $error_message, $response ); } private function categorize_with_gemini( $rewritten_content ) { // Use expanded list to avoid "Loterias" trap $category_list = $this->get_expanded_category_list(); $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 1500 ) . '...'; $base_prompt = str_replace( array( '{rewritten_content}', '{current_categories}' ), array( $prompt_context_content, $category_list ), $this->options['prompt_category'] ); // Translate prompt to target language for the category name $prompt = $base_prompt . $this->get_language_instruction() . " Output ONLY the category name."; $payload = array( 'contents' => array( array( 'parts' => array( array( 'text' => $prompt ) ) ) ), 'generationConfig' => array( 'responseMimeType' => 'text/plain', 'maxOutputTokens' => 20, ), ); $all_models = $this->get_all_gemini_models(); $model_name = $this->options['gemini_model_selection'] ?? 'gemini-1.5-flash-001'; $version = $all_models[ $model_name ]['version'] ?? 'v1beta'; $response = $this->call_gemini_api( $model_name, $version, 'generateContent', $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['candidates'][0]['content']['parts'][0]['text'] ) ) { return sanitize_text_field( trim( $response['candidates'][0]['content']['parts'][0]['text'], '"\'., ' ) ); } return new WP_Error( 'gemini_category_error', __( 'Unexpected response from Gemini API for categorization.', 'ainp-ai-native-publisher' ), $response ); } private function categorize_with_groq( $rewritten_content ) { // Use expanded list to avoid "Loterias" trap $category_list = $this->get_expanded_category_list(); $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 1500 ) . '...'; $base_prompt = str_replace( array( '{rewritten_content}', '{current_categories}' ), array( $prompt_context_content, $category_list ), $this->options['prompt_category'] ); $prompt = $base_prompt . $this->get_language_instruction(); $payload = array( // Changed system prompt to force classification even with minimal data and use expanded list 'messages' => array( array( 'role' => 'system', 'content' => 'You are a content classifier. Analyze the text and choose the single best category from the user provided list. If no exact match exists, choose the closest generic topic. Do not refuse to answer. Output ONLY the category name.' ), array( 'role' => 'user', 'content' => $prompt ) ), 'model' => $this->options['groq_model_selection'] ?? 'llama-3.1-8b-instant', 'max_tokens' => 20, ); $response = $this->call_groq_api( $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['choices'][0]['message']['content'] ) ) { return sanitize_text_field( trim( $response['choices'][0]['message']['content'], '"\'., ' ) ); } return new WP_Error( 'groq_category_error', __( 'Unexpected response from Groq API for categorization.', 'ainp-ai-native-publisher' ), $response ); } private function generate_tags_with_gemini( $rewritten_content ) { $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 1500 ) . '...'; $base_prompt = str_replace( '{rewritten_content}', $prompt_context_content, $this->options['prompt_tags'] ); $prompt = $base_prompt . " " . $this->get_language_instruction() . " Ensure the output is a VALID JSON array of strings."; $json_schema = array( 'type' => 'OBJECT', 'properties' => array( 'tags' => array( 'type' => 'ARRAY', 'items' => array( 'type' => 'STRING' ), ), ), 'required' => array( 'tags' ), ); $payload = array( 'contents' => array( array( 'parts' => array( array( 'text' => $prompt ) ) ) ), 'generationConfig' => array( 'responseMimeType' => 'application/json', 'responseSchema' => $json_schema, 'maxOutputTokens' => 100, ), ); $all_models = $this->get_all_gemini_models(); $model_name = $this->options['gemini_model_selection'] ?? 'gemini-1.5-flash-001'; $version = $all_models[ $model_name ]['version'] ?? 'v1beta'; $response = $this->call_gemini_api( $model_name, $version, 'generateContent', $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['candidates'][0]['content']['parts'][0]['text'] ) ) { return $this->format_ia_response_tags( $response['candidates'][0]['content']['parts'][0]['text'] ); } return new WP_Error( 'gemini_tags_error', __( 'Unexpected JSON response structure from Gemini API for tags.', 'ainp-ai-native-publisher' ), $response ); } private function generate_tags_with_groq( $rewritten_content ) { // Increased context to 1500 $prompt_context_content = mb_substr( wp_strip_all_tags( $rewritten_content ), 0, 1500 ) . '...'; $base_prompt = str_replace( '{rewritten_content}', $prompt_context_content, $this->options['prompt_tags'] ); // Refined instructions to reduce API Error 400 and Force JSON $prompt = $base_prompt . " " . $this->get_language_instruction() . " Output MUST be a JSON object with a single key 'tags' containing an array of strings."; $payload = array( 'messages' => array( array( 'role' => 'user', 'content' => $prompt ) ), 'model' => $this->options['groq_model_selection'] ?? 'llama-3.1-8b-instant', 'response_format' => array( 'type' => 'json_object' ), 'max_tokens' => 150, // Increased slightly to prevent cutoff ); $response = $this->call_groq_api( $payload ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response['choices'][0]['message']['content'] ) ) { return $this->format_ia_response_tags( $response['choices'][0]['message']['content'] ); } return new WP_Error( 'groq_tags_error', __( 'Unexpected JSON response structure from Groq API for tags.', 'ainp-ai-native-publisher' ), $response ); } // --- Private Helper Functions (Formatting & API Calls) --- private function format_ia_response_content( $text ) { $text = str_replace( array( "\r\n", "\r" ), "\n", $text ); $text = trim( $text ); $text = preg_replace( '/^##\s*(.+?)\s*$/m', "\n\n

$1

\n\n", $text ); $text = preg_replace( '/^###\s*(.+?)\s*$/m', "\n\n

$1

\n\n", $text ); $text = preg_replace( '/^\s*\*\*\s*(.+?)\s*\*\*\s*$/m', "\n\n

$1

\n\n", $text ); $text = preg_replace( "/\n{3,}/", "\n\n", $text ); return trim( $text ); } private function format_ia_response_title( $raw_text, $original_title ) { $generated_title = trim( $raw_text ); $generated_title = trim( $generated_title, '"\'`*“”‘’«».?!,' ); $intro_patterns = array( '/^##\s*(.*?)\s*$/m', '/^###\s*(.*?)\s*$/m', '/^new title:?\s*/i', '/^suggested title:?\s*/i', '/^optimized title:?\s*/i', '/^title:?\s*/i', '/^\d+\.\s*/', ); foreach ( $intro_patterns as $pattern ) { $generated_title = preg_replace( $pattern, '', $generated_title ); } $generated_title = trim( $generated_title, '"\'`*“”‘’«».?!, ' ); if ( empty( $generated_title ) || str_word_count( $generated_title ) < 2 ) { $this->logger->add_log_entry( 'warning', sprintf( /* translators: 1: Generated title (escaped), 2: Original title (escaped) */ __( 'AI-generated title ("%1$s") for "%2$s" is too short or empty. Using original title.', 'ainp-ai-native-publisher' ), esc_html( $raw_text ), esc_html( $original_title ) ) ); return $original_title; } return mb_substr( $generated_title, 0, self::MAX_TITLE_LENGTH ); } /** * Robustly decodes JSON tags, handling object wrappers and diverse keys. */ private function format_ia_response_tags( $json_text ) { // 1. Clean Markdown Code Blocks (```json ... ```) $clean_result = preg_replace( '/^```json\s*|\s*```$/i', '', trim( $json_text ) ); $clean_result = preg_replace( '/^```\s*|\s*```$/i', '', $clean_result ); // 2. Decode $data = json_decode( $clean_result, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { // Fallback: Try to find a JSON array within text using regex if ( preg_match( '/\[.*?\]/s', $clean_result, $matches ) ) { $data = json_decode( $matches[0], true ); } } if ( ! is_array( $data ) ) { $this->logger->add_log_entry( 'error', __( 'Failed to decode JSON for tags or unexpected format.', 'ainp-ai-native-publisher' ), 'Raw API Response: ' . esc_html( substr( $json_text, 0, 300 ) ) . '...' ); return new WP_Error( 'json_parse_error', __( 'Failed to decode JSON for tags or unexpected format.', 'ainp-ai-native-publisher' ) ); } // 3. Flatten Logic: Check if it's an object wrapping the array (e.g. {"keywords": [...]}) // If keys are not numeric (0, 1, 2), it's likely an associative array/object. if ( array_keys( $data ) !== range( 0, count( $data ) - 1 ) ) { // Look for the first value that is an array $found_array = false; foreach ( $data as $key => $value ) { if ( is_array( $value ) ) { $data = $value; $found_array = true; break; } } // If no array found inside, try converting values to string if simple object if ( ! $found_array ) { // Maybe it's {"tag1": "value", "tag2": "value"} - rare but possible $data = array_keys( $data ); } } // 4. Sanitize and Limit $sanitized_tags = array_filter( array_map( 'sanitize_text_field', $data ) ); return array_slice( $sanitized_tags, 0, self::MAX_TAG_COUNT ); } private function call_gemini_api( $model_name, $version, $method, $payload, $is_test = false ) { $api_key = $this->options['gemini_api_key'] ?? ''; if ( empty( $api_key ) ) { return new WP_Error( 'api_key_missing', __( 'Gemini API key is not configured.', 'ainp-ai-native-publisher' ) ); } $url = AINP_GEMINI_API_BASE_URL . $version . '/models/' . $model_name . ':' . $method . '?key=' . $api_key; $args = array( 'method' => 'POST', 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'timeout' => 60, ); return $this->make_api_call( $url, $args, 'Gemini', $model_name, $is_test ); } private function call_groq_api( $payload, $is_test = false ) { $api_key = $this->options['groq_api_key'] ?? ''; if ( empty( $api_key ) ) { return new WP_Error( 'api_key_missing', __( 'Groq API key is not configured.', 'ainp-ai-native-publisher' ) ); } $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $payload ), 'timeout' => 60, ); $model_name = $payload['model'] ?? 'unknown'; return $this->make_api_call( AINP_GROQ_API_URL, $args, 'Groq', $model_name, $is_test ); } /** * Makes a POST request to the Google Imagen API endpoint. * Kept for potential Pro upgrade hooks but safe in Free version. */ public function call_imagen_api( $post_id, $title, $content_summary, $is_test = false ) { $api_key = $this->options['imagen_api_key'] ?? ''; $project_id = $this->options['imagen_project_id'] ?? ''; $location = $this->options['imagen_location'] ?? ''; if ( empty( $api_key ) || empty( $project_id ) || empty( $location ) ) { return new WP_Error( 'imagen_disabled', __( 'Imagen is disabled or missing credentials.', 'ainp-ai-native-publisher' ) ); } $url = sprintf( 'https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/imagen-3.0-generate-002:predict', rawurlencode( $location ), rawurlencode( $project_id ), rawurlencode( $location ) ); $prompt = str_replace( array( '{title}', '{content_summary}' ), array( wp_strip_all_tags( $title ), wp_trim_words( wp_strip_all_tags( $content_summary ), 300, '...' ) ), $this->options['prompt_imagen'] ); $payload = array( 'instances' => array( array( 'prompt' => $prompt ) ), 'parameters' => array( 'sampleCount' => 1 ), ); $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $payload ), 'timeout' => 90, ); return $this->make_api_call( $url, $args, 'Imagen', 'Vertex AI Imagen', $is_test ); } public function call_unsplash_api( $query, $is_test = false ) { $access_key = $this->options['unsplash_access_key'] ?? ''; if ( empty( $access_key ) ) { return new WP_Error( 'unsplash_key_missing', __( 'Unsplash Access Key is not configured.', 'ainp-ai-native-publisher' ) ); } $url = AINP_UNSPLASH_API_URL . 'search/photos'; $params = array( 'query' => $query, 'per_page' => 1, 'orientation' => 'landscape', 'client_id' => $access_key, ); $url = add_query_arg( $params, $url ); $args = array( 'method' => 'GET', 'headers' => array( 'Accept-Version' => 'v1', ), 'timeout' => 30, ); return $this->make_api_call( $url, $args, 'Unsplash', 'Search Photos', $is_test ); } private function make_api_call( $url, $args, $api_name, $model_name = '', $is_test = false ) { $max_retries = 3; $response = null; for ( $attempt = 1; $attempt <= $max_retries; $attempt++ ) { $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { if ( $attempt === $max_retries ) { /* translators: 1: API Name, 2: Model Name, 3: Attempt number, 4: Error message */ $error_message = sprintf( __( 'WP_Error calling %1$s API (%2$s) after %3$d attempts: %4$s', 'ainp-ai-native-publisher' ), $api_name, $model_name, $max_retries, $response->get_error_message() ); $this->logger->add_log_entry( 'error', $error_message ); return $response; } sleep( pow( 2, $attempt ) ); continue; } $response_code = wp_remote_retrieve_response_code( $response ); if ( 503 === $response_code || 429 === $response_code ) { if ( $attempt === $max_retries ) { break; } /* translators: 1: API Name, 2: Model Name, 3: Response Code, 4: Attempt number */ $this->logger->add_log_entry( 'warning', sprintf( __( '%1$s API (%2$s) returned %3$d on attempt %4$d. Retrying...', 'ainp-ai-native-publisher' ), $api_name, $model_name, $response_code, $attempt ) ); sleep( pow( 2, $attempt ) ); continue; } if ( $response_code >= 200 && $response_code < 300 ) { $response_body = wp_remote_retrieve_body( $response ); $decoded_body = json_decode( $response_body, true ); if ( json_last_error() === JSON_ERROR_NONE ) { return $decoded_body; } else { /* translators: 1: API Name, 2: Model Name, 3: Response Code */ $error_message = sprintf( __( 'Failed to decode JSON response from %1$s API (%2$s). Code: %3$d', 'ainp-ai-native-publisher' ), $api_name, $model_name, $response_code ); $this->logger->add_log_entry( 'error', $error_message, 'Response Body Snippet: ' . esc_html( substr( $response_body, 0, 300 ) ) . '...' ); return new WP_Error( strtolower( $api_name ) . '_json_decode_error', $error_message, array( 'code' => $response_code, 'body' => $response_body ) ); } } if ( $is_test && 'Imagen' === $api_name && 400 === $response_code ) { return new WP_Error( 'imagen_test_success_400', __( 'Imagen API test received expected 400 Bad Request response.', 'ainp-ai-native-publisher' ), array( 'code' => $response_code ) ); } $response_body = wp_remote_retrieve_body( $response ); $decoded_body = json_decode( $response_body, true ); $error_details = ''; if ( $decoded_body && isset( $decoded_body['error']['message'] ) ) { $error_details = esc_html( $decoded_body['error']['message'] ); } elseif ( $decoded_body && isset( $decoded_body['errors'] ) && is_array( $decoded_body['errors'] ) ) { $error_details = esc_html( implode( ', ', $decoded_body['errors'] ) ); } /* translators: 1: API Name, 2: Model Name, 3: Response Code */ $error_message = sprintf( __( 'HTTP Error calling %1$s API (%2$s): Code %3$d', 'ainp-ai-native-publisher' ), $api_name, $model_name, $response_code ); $this->logger->add_log_entry( 'error', $error_message, $error_details . ' | Response Body Snippet: ' . esc_html( substr( $response_body, 0, 300 ) ) ); return new WP_Error( strtolower( $api_name ) . '_api_error', $error_message, array( 'code' => $response_code, 'body' => $decoded_body ?: $response_body ) ); } /* translators: 1: API Name, 2: Model Name, 3: Max Retries, 4: Last Code */ $final_error_message = sprintf( __( '%1$s API (%2$s) remained unavailable after %3$d attempts (last code: %4$s).', 'ainp-ai-native-publisher' ), $api_name, $model_name, $max_retries, $response_code ?? 'N/A' ); $this->logger->add_log_entry( 'error', $final_error_message ); return new WP_Error( strtolower( $api_name ) . '_unavailable', $final_error_message, array( 'code' => $response_code ?? null ) ); } }