diff --git a/.wp-env.json b/.wp-env.json index 9e14b52..ac0c3ea 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,4 +1,8 @@ { - "plugins": ["./ai-entries/"], - "phpVersion": "7.4" + "plugins": [ + "./ai-entries/", + "https://downloads.wordpress.org/plugin/plugin-check.zip" + ], + "phpVersion": "7.4", + "core": null } diff --git a/ai-entries/README.md b/ai-entries/README.md index b9d1b6f..2e1d24d 100644 --- a/ai-entries/README.md +++ b/ai-entries/README.md @@ -2,7 +2,7 @@ Contributors: Julio Bermúdez Tested up to: 6.6 -Stable tag: 1.0.3 +Stable tag: 1.0.7 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: plugin best practices, accessibility, performance, security, automation diff --git a/ai-entries/ai-entries.php b/ai-entries/ai-entries.php index 785d0e8..c015c45 100644 --- a/ai-entries/ai-entries.php +++ b/ai-entries/ai-entries.php @@ -2,7 +2,7 @@ /** * Plugin Name: AI Entries * Description: Automates the creation of standard WordPress posts. - * Version: 1.0.3 + * Version: 1.0.7 * Requires at least: 5.2 * Requires PHP: 7.2 * Author: Julio Bermúdez diff --git a/ai-entries/includes/class-ai-entries-api.php b/ai-entries/includes/class-ai-entries-api.php index ac81793..8ecbf1f 100644 --- a/ai-entries/includes/class-ai-entries-api.php +++ b/ai-entries/includes/class-ai-entries-api.php @@ -2,62 +2,93 @@ class AIEntries_API { + public static $responses = array(); + public static function fetch_news() + { + $api_base_url = 'https://newsapi.org/v2/everything'; + + // Construir la URL completa con los parámetros + $url = add_query_arg(array( + 'q' => get_option('AIEntries_question', ''), + 'apiKey' => get_option('AIEntries_news_api_key', ''), + 'pageSize' => get_option('AIEntries_num_calls', 1), + ), $api_base_url); + + // Realizar la solicitud GET utilizando wp_remote_get + $response = wp_remote_get($url, array('headers' => array('User-Agent' => get_option('AIEntries_news_api_key', '')))); + + // Verificar si la solicitud fue exitosa + if (is_wp_error($response)) { + return "Error: " . $response->get_error_message(); + } + + $body = wp_remote_retrieve_body($response); + + // Decodificar el cuerpo de la respuesta JSON + $data = json_decode($body, true); + + // Devolver los datos decodificados + return $data['articles']; + } public static function call($question, $api_key, $category_name, $iterator = "") { - // URL for the API call - $url = 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key=' . $api_key; - - // Request arguments - $args = array( - 'timeout' => 60, - 'body' => wp_json_encode(array( - "contents" => array( - array( - "parts" => array( - array( - "text" => "List 1 " . $iterator . " article about " . $question . ". Using this JSON schema :{'title': str,'content':str} (Return only the JSON String without spaces) the title must be good for SEO and the content must be in html string", + $news_articles = self::fetch_news(); + + foreach ($news_articles as $key => $value) { + // URL for the API call + $url = 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key=' . $api_key; + + // Request arguments + $args = array( + 'timeout' => 60, + 'body' => wp_json_encode(array( + "contents" => array( + array( + "parts" => array( + array( + "text" => "Analize this article : {'title':'" . wp_json_encode($value['title']) . "','description':'" . wp_json_encode($value['description']) . "','content':'" . wp_json_encode($value['content']) . "'} . Now write 1 related original article in english using this JSON schema : {'title': str,'content':str} (Return only the JSON String without spaces) the title must be good for SEO and the content must be in html string", + ), ), ), ), + )), + 'headers' => array( + 'Content-Type' => 'application/json', ), - )), - 'headers' => array( - 'Content-Type' => 'application/json', - ), - 'method' => 'POST', - ); - - // Response - $response = wp_remote_post($url, $args); - - // If anything goes wrong - if (is_wp_error($response)) { - return new WP_Error('api_error', $response->get_error_message()); - } + 'method' => 'POST', + ); - // Retrieve body - $body = wp_remote_retrieve_body($response); + // Response + $response = wp_remote_post($url, $args); - // Format data - if (empty($body)) { - return new WP_Error('api_error', 'Empty response from API.'); - } + // If anything goes wrong + if (is_wp_error($response)) { + return new WP_Error('api_error', $response->get_error_message()); + } - $data = json_decode($body, true); + // Retrieve body + $body = wp_remote_retrieve_body($response); + // Format data + if (empty($body)) { + return new WP_Error('api_error', 'Empty response from API.'); + } - if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) { - return new WP_Error('api_error', 'Invalid API response structure.'); - } + $data = json_decode($body, true); - // AI Post - $article = json_decode($data['candidates'][0]['content']['parts'][0]['text'], true); + if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) { + return new WP_Error('api_error', 'Invalid API response structure.'); + } - if (!isset($article['title']) || !isset($article['content'])) { - return new WP_Error('api_error', 'API response does not contain title or content.'); + // AI Post + $article = json_decode($data['candidates'][0]['content']['parts'][0]['text'], true); + + if (!isset($article['title']) || !isset($article['content'])) { + return new WP_Error('api_error', 'API response does not contain title or content.'); + } + + self::create_new_entry($article['title'], $article['content'], $category_name); } - - return self::create_new_entry($article['title'], $article['content'], $category_name); } private static function create_new_entry($title, $content, $category_name) @@ -86,27 +117,29 @@ private static function create_new_entry($title, $content, $category_name) if (is_wp_error($post_id)) { return new WP_Error('insert_error', $post_id->get_error_message()); } else { - $base64_image = self::generate_post_image_with_AI($title); - self::set_featured_image_from_base64($base64_image, $post_id); + + self::generate_post_image_with_AI($title, $post_id); + //self::set_featured_image_from_base64($base64_image, $post_id); wp_clear_scheduled_hook('AIEntries_daily_cron_job'); - wp_schedule_event(strtotime('now') + (1 * 60 * 60) , 'hourly', 'AIEntries_daily_cron_job'); + wp_schedule_event(strtotime('now') + (1 * 60 * 60), 'hourly', 'AIEntries_daily_cron_job'); + + array_push(self::$responses,get_post($post_id)); - return get_post($post_id); } } return new WP_Error('permission_error', 'You do not have permission to publish posts.'); } - private static function generate_post_image_with_AI($title) + private static function generate_post_image_with_AI($title, $post_id) { $base_url = 'https://api.stability.ai'; $url = "$base_url/v1/generation/stable-diffusion-v1-6/text-to-image"; $api_key_stable_diffusion = get_option('AIEntries_api_key_stable_diffusion', ''); $body = wp_json_encode(array( - "text_prompts" => array(array("text" => $title)), + "text_prompts" => array(array("text" => $title .'. without texts in the image.')), "cfg_scale" => 7, "height" => 1024, "width" => 1024, @@ -114,8 +147,8 @@ private static function generate_post_image_with_AI($title) "steps" => 30, )); - $response = wp_remote_post($url, array( - 'timeout' => 600, + $response = wp_remote_post($url, array( + 'timeout'=>600, 'method' => 'POST', 'headers' => array( 'Content-Type' => 'application/json', @@ -124,17 +157,15 @@ private static function generate_post_image_with_AI($title) ), 'body' => $body, )); - + if (is_wp_error($response)) { return ''; } - + $body_request = json_decode($response['body'], true); - return $body_request['artifacts'][0]['base64']; - } + + $base64_image = $body_request['artifacts'][0]['base64']; - private static function set_featured_image_from_base64($base64_image, $post_id) - { if (!is_int($post_id)) { return false; } @@ -173,10 +204,9 @@ private static function set_featured_image_from_base64($base64_image, $post_id) require_once ABSPATH . 'wp-admin/includes/image.php'; $attach_data = wp_generate_attachment_metadata($attach_id, $file_path); + wp_update_attachment_metadata($attach_id, $attach_data); - set_post_thumbnail($post_id, $attach_id); - return true; + set_post_thumbnail($post_id, $attach_id); } - } diff --git a/ai-entries/includes/class-ai-entries-cron.php b/ai-entries/includes/class-ai-entries-cron.php index f6d7f7d..49937eb 100644 --- a/ai-entries/includes/class-ai-entries-cron.php +++ b/ai-entries/includes/class-ai-entries-cron.php @@ -2,27 +2,7 @@ class AIEntries_Cron { - // Function to check and run the function every 5 minutes - public static function my_six_hour_function() - { - - AIEntries_Cron::daily_task(); // Execute the function - - } - - // Function to check and run the function every 6 hours - public static function check_six_hour_function() - { - $last_executed_time = get_transient('last_six_hour_execution'); - - // Check if it's time to run the function (6 hours have passed) - if (!$last_executed_time || $last_executed_time < strtotime('-6 hours')) { - AIEntries_Cron::my_six_hour_function(); // Execute the function - set_transient('last_six_hour_execution', strtotime('now')); // Update the last execution time - } - } - - public function daily_task() + public static function daily_task() { $question = get_option('AIEntries_question', ''); $num_calls = get_option('AIEntries_num_calls', 1); @@ -44,54 +24,43 @@ public static function show_all_cron_tasks() if (empty($cron)) { return 'No tasks scheduled.'; } - - $output = ''; - $output .= ''; - foreach ($cron as $timestamp => $cronhooks) { - foreach ((array) $cronhooks as $hook => $events) { - if ($hook == 'AIEntries_daily_cron_job') { - $callbacks = array(); - foreach ((array) $events as $event) { - if (isset($GLOBALS['wp_filter'][$hook])) { - $callbacks[] = $GLOBALS['wp_filter'][$hook]; - } - } - //print_r($callbacks); - foreach ($callbacks as $priority => $callback) { - foreach ($callback->callbacks as $function_data) { - foreach ($function_data as $function_parts) { - $output .= ''; - $output .= ''; - $output .= ''; - $output .= ''; - $output .= ''; + if (wp_next_scheduled('AIEntries_daily_cron_job')) { + $output = '

Next Excecution

Hook Name

Function Name

' . esc_html(gmdate('Y-m-d H:i:s', $timestamp)) . '

' . esc_html($hook) . '

' . esc_html(strval($function_parts['function'][1] ? $function_parts['function'][1] : $function_parts['function'][0])) . '

'; + $output .= ''; + foreach ($cron as $timestamp => $cronhooks) { + foreach ((array) $cronhooks as $hook => $events) { + if ($hook == 'AIEntries_daily_cron_job') { + $callbacks = array(); + foreach ((array) $events as $event) { + if (isset($GLOBALS['wp_filter'][$hook])) { + $callbacks[] = $GLOBALS['wp_filter'][$hook]; } - } - } - /* // Suponiendo que $array contiene el array proporcionado - foreach ($callbacks as $priority => $callbacks) { - foreach ($callbacks as $callback_name => $callback_info) { - $function_info = $callback_info['function']; - - // Obtener el objeto/clase y el nombre de la función - $function_object = $function_info[0]; - $function_name = $function_info[1]; + //print_r($callbacks); + foreach ($callbacks as $priority => $callback) { + foreach ($callback->callbacks as $function_data) { + foreach ($function_data as $function_parts) { + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + } - echo 'Prioridad: ' . $priority . '
'; - echo 'Función: ' . $function_object . '::' . $function_name . '
'; + } + } - } - } */ + } } - - } + } + echo wp_kses_post($output); } + if (!wp_next_scheduled('AIEntries_daily_cron_job')) { - echo "\n \n No excecutions scheduled for: " . esc_html($hook_name); + return "\n \n No excecutions scheduled"; } - echo esc_html($output); + } } diff --git a/ai-entries/includes/class-ai-entries-settings.php b/ai-entries/includes/class-ai-entries-settings.php index 1560214..7a10170 100644 --- a/ai-entries/includes/class-ai-entries-settings.php +++ b/ai-entries/includes/class-ai-entries-settings.php @@ -10,7 +10,8 @@ public static function add_menu_page() 'AIEntries', 'manage_options', 'AIEntries-settings', - [self::class, 'settings_page'] + [self::class, 'settings_page'], + 'dashicons-visibility' ); return true; } @@ -30,20 +31,21 @@ public static function settings_page() if (isset($_POST['submit'])) { update_option('AIEntries_question', sanitize_text_field($_POST['question'])); update_option('AIEntries_num_calls', intval($_POST['num_calls'])); + update_option('AIEntries_news_api_key', sanitize_text_field($_POST['news_api_key'])); update_option('AIEntries_api_key', sanitize_text_field($_POST['api_key'])); update_option('AIEntries_category', sanitize_text_field($_POST['category'])); update_option('AIEntries_api_key_stable_diffusion', sanitize_text_field($_POST['api_key_stable_diffusion'])); $responses = []; $errors = []; - for ($i = 0; $i < intval($_POST['num_calls']); $i++) { - $response = AIEntries_API::call($_POST['question'], $_POST['api_key'], $_POST['category'], $i > 0 ? '' : 'more distinct'); - if (!is_wp_error($response)) { - $responses[] = $response; - } else { - $errors[] = $response->get_error_message(); - } + + $response = AIEntries_API::call($_POST['question'], $_POST['api_key'], $_POST['category']); + if (!is_wp_error($response)) { + $responses[] = $response; + } else { + $errors[] = $response->get_error_message(); } + } else { $responses = []; $errors = []; @@ -52,6 +54,7 @@ public static function settings_page() $question = get_option('AIEntries_question', ''); $num_calls = get_option('AIEntries_num_calls', 1); $api_key = get_option('AIEntries_api_key', ''); + $news_api_key = get_option('AIEntries_news_api_key', ''); $category = get_option('AIEntries_category', ''); $api_key_stable_diffusion = get_option('AIEntries_api_key_stable_diffusion', ''); diff --git a/ai-entries/includes/class-ai-entries.php b/ai-entries/includes/class-ai-entries.php index 7575007..d1a2b94 100644 --- a/ai-entries/includes/class-ai-entries.php +++ b/ai-entries/includes/class-ai-entries.php @@ -33,8 +33,7 @@ private function includes() private function init_hooks() { - add_action('admin_menu', ['AIEntries_Settings', 'add_menu_page']); - add_action('wp', ['AIEntries_Cron', 'check_six_hour_function']); + add_action('admin_menu', ['AIEntries_Settings', 'add_menu_page']); add_action('AIEntries_daily_cron_job', ['AIEntries_Cron', 'daily_task']); } diff --git a/ai-entries/includes/settings-page.php b/ai-entries/includes/settings-page.php index 2a07ac0..55b9534 100644 --- a/ai-entries/includes/settings-page.php +++ b/ai-entries/includes/settings-page.php @@ -1,5 +1,6 @@

AIEntries Settings

@@ -15,7 +16,12 @@ -
+
+ +
+

Note: You can get one for free here

@@ -42,9 +48,9 @@ - +

Posts Created by GEMINI's API Call:

- +
ID)); ?>
@@ -52,6 +58,6 @@

mantain and scale this plugin

Wordpress Cron tasks scheduled by this plugin:

diff --git a/ai-entries/tests/classes/AIEntriesAPITest.php b/ai-entries/tests/classes/AIEntriesAPITest.php index 5d1c378..7f0db07 100644 --- a/ai-entries/tests/classes/AIEntriesAPITest.php +++ b/ai-entries/tests/classes/AIEntriesAPITest.php @@ -1,30 +1,27 @@ (object)['term_id' => 1] + 'return' => (object) ['term_id' => 1], ]); WP_Mock::userFunction('current_user_can', [ - 'return' => true + 'return' => true, ]); WP_Mock::userFunction('wp_insert_post', [ - 'return' => 123 + 'return' => 123, ]); WP_Mock::userFunction('get_option', [ - 'return' => 'api_key_stable_diffusion_value' + 'return' => 'api_key_stable_diffusion_value', ]); WP_Mock::userFunction('wp_upload_dir'); @@ -67,11 +64,11 @@ public function testCallSuccess() { WP_Mock::userFunction('WP_Filesystem'); WP_Mock::userFunction('wp_insert_attachment', [ - 'return' => 456 + 'return' => 456, ]); WP_Mock::userFunction('wp_generate_attachment_metadata', [ - 'return' => [] + 'return' => [], ]); WP_Mock::userFunction('wp_update_attachment_metadata'); @@ -79,37 +76,57 @@ public function testCallSuccess() { WP_Mock::userFunction('set_post_thumbnail'); WP_Mock::userFunction('get_post', [ - 'return' => (object)[ + 'return' => (object) [ 'ID' => 123, 'post_title' => 'Test Title', - 'post_content' => '

Test Content

' - ] + 'post_content' => '

Test Content

', + ], ]); - WP_Mock::userFunction('wp_json_encode'); + WP_Mock::userFunction('wp_json_encode'); WP_Mock::userFunction('is_wp_error'); - WP_Mock::userFunction('wp_remote_retrieve_body'); - + WP_Mock::userFunction('wp_remote_retrieve_body'); // Assert that the returned result is as expected - - $this->assertSame( true, true ); + + $this->assertSame(true, true); } - public function testCallError() { + public function testCallError() + { $question = 'example question'; $api_key = 'test_api_key'; $category_name = 'Test Category'; $iterator = '1'; - + + WP_Mock::userFunction('add_query_arg'); + WP_Mock::userFunction('wp_remote_get'); + WP_Mock::userFunction('wp_remote_post'); // Simulate an error response from wp_remote_post - WP_Mock::userFunction('wp_remote_post', [ - 'return' => new WP_Error( 'api_error', 'Something went wrong' ), + WP_Mock::userFunction('wp_remote_retrieve_body', [ + 'return' => '{ + "status": "ok", + "totalResults": 268, + "articles": [ + { + "source": { + "id": null, + "name": "Hotnews.ro" + }, + "author": "Mihai Bianca", + "title": "fanatik.ro: I se spune Maldive de Europa. Se ajunge ușor din România și e de 3 ori mai ieftină decât Grecia sau Turcia", + "description": "Descoperă țara din Balcani cu cea mai impetuoasă dezvoltare la nivel de turism. Este catalogată ca având plaje la fel ca în Maldive. Maldive de Europa, adică un joc frumos de cuvinte, dar și un…", + "url": "http://hotnews.ro/fanatik-ro-i-se-spune-maldive-de-europa-se-ajunge-usor-din-romania-si-e-de-3-ori-mai-ieftina-decat-grecia-sau-turcia-1532267", + "urlToImage": "https://hotnews.ro/wp-content/uploads/2024/06/Screenshot-2024-06-29-105522.png", + "publishedAt": "2024-06-29T07:56:14Z", + "content": "Descoper ara din Balcani cu cea mai impetuoas dezvoltare la nivel de turism. Este catalogat ca având plaje la fel ca în Maldive.\r\nMaldive de Europa, adic un joc frumos de cuvinte, dar i un loc pentru… [+310 chars]" + } + ] + }', ]); - - $result = AIEntries_API::call( $question, $api_key, $category_name, $iterator ); + $result = AIEntries_API::call($question, $api_key, $category_name, $iterator); // Verify that the result is an instance of WP_Error - $this->assertInstanceOf( 'WP_Error', $result ); + $this->assertInstanceOf('WP_Error', $result); } } diff --git a/ai-entries/tests/classes/AIEntriesCronTest.php b/ai-entries/tests/classes/AIEntriesCronTest.php index e822531..5574ec0 100644 --- a/ai-entries/tests/classes/AIEntriesCronTest.php +++ b/ai-entries/tests/classes/AIEntriesCronTest.php @@ -2,25 +2,14 @@ require_once __DIR__ . '/../../includes/class-ai-entries-cron.php'; - class AIEntriesCronTest extends WP_Mock\Tools\TestCase { - - public function test_my_six_hour_function() + public function test_daily_task() { - // Call the method to test - $this->assertNull(AIEntries_Cron::my_six_hour_function()); - } + WP_Mock::userFunction('_get_cron_array'); + $this->assertNull(AIEntries_Cron::daily_task()); - public function test_check_six_hour_function() - { - - // Call the method to test - WP_Mock::userFunction('get_transient'); - WP_Mock::userFunction('set_transient'); - $this->assertNull(AIEntries_Cron::check_six_hour_function()); } - public function test_show_all_cron_tasks() { WP_Mock::userFunction('_get_cron_array'); diff --git a/ai-entries/tests/classes/AIEntriesSettingsTest.php b/ai-entries/tests/classes/AIEntriesSettingsTest.php index bbdbe46..1ca926e 100644 --- a/ai-entries/tests/classes/AIEntriesSettingsTest.php +++ b/ai-entries/tests/classes/AIEntriesSettingsTest.php @@ -48,18 +48,41 @@ public function testSettingsPageNoSubmit() public function testSettingsPageSubmit() { WP_Mock::userFunction('sanitize_text_field'); - WP_Mock::userFunction('wp_json_encode'); - WP_Mock::userFunction('wp_remote_post'); - WP_Mock::userFunction('is_wp_error'); - WP_Mock::userFunction('wp_remote_retrieve_body'); + WP_Mock::userFunction('wp_json_encode'); + WP_Mock::userFunction('is_wp_error'); Mockery::mock('WP_Error'); WP_Mock::userFunction('get_post_permalink'); WP_Mock::userFunction('get_the_title'); WP_Mock::userFunction('_get_cron_array'); + WP_Mock::userFunction('add_query_arg'); + WP_Mock::userFunction('add_query_arg'); + WP_Mock::userFunction('wp_remote_post'); + // Simulate an error response from wp_remote_post + WP_Mock::userFunction('wp_remote_retrieve_body', [ + 'return' => '{ + "status": "ok", + "totalResults": 268, + "articles": [ + { + "source": { + "id": null, + "name": "Hotnews.ro" + }, + "author": "Mihai Bianca", + "title": "fanatik.ro: I se spune Maldive de Europa. Se ajunge ușor din România și e de 3 ori mai ieftină decât Grecia sau Turcia", + "description": "Descoperă țara din Balcani cu cea mai impetuoasă dezvoltare la nivel de turism. Este catalogată ca având plaje la fel ca în Maldive. Maldive de Europa, adică un joc frumos de cuvinte, dar și un…", + "url": "http://hotnews.ro/fanatik-ro-i-se-spune-maldive-de-europa-se-ajunge-usor-din-romania-si-e-de-3-ori-mai-ieftina-decat-grecia-sau-turcia-1532267", + "urlToImage": "https://hotnews.ro/wp-content/uploads/2024/06/Screenshot-2024-06-29-105522.png", + "publishedAt": "2024-06-29T07:56:14Z", + "content": "Descoper ara din Balcani cu cea mai impetuoas dezvoltare la nivel de turism. Este catalogat ca având plaje la fel ca în Maldive.\r\nMaldive de Europa, adic un joc frumos de cuvinte, dar i un loc pentru… [+310 chars]" + } + ] + }', + ]); // Mock POST request with submit $_POST = [ 'submit' => true, - 'aic_entries_nonce' => 'fake_nonce', + 'aic_entries_nonce' => 'fake_nonce', 'question' => 'test question', 'num_calls' => 1, 'api_key' => 'test_api_key', diff --git a/readme.md b/readme.md index efbc799..64f08db 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,55 @@ # AIEntries Plugin -This plugin uses Google artificial intelligence (GEMINI) and stability.AI to automate the creation of standard WordPress posts based on configurable parameters from the WordPress admin view. +This plugin uses Google artificial intelligence (GEMINI) and stability.AI to automate the creation of standard WordPress posts based on configurable parameters from the WordPress admin view. It can create posts from any topic you configure from the administrator view. To ensure quality content, this tool is integrated with several free-to-use APIs to fulfill its functionality. + +## This plugin is aware of Google's advice on AI content: +https://developers.google.com/search/blog/2023/02/google-search-and-ai-content?hl=es-419 + +In order to follow they guidelines we are doing these processes: + +### News API: + +we use this api to get real articles + +https://newsapi.org/docs + +### Google Gemini API: + +We use this api to generate original content based in real one + +https://ai.google.dev/gemini-api?hl=es-419 + +### Stability AI: + +We use this api to generate post's featured imaged based on ai generated article's title . + +https://stability.ai/ + + +### DISCLAIMER : THIS IS AN IN PROGRESS PROJECT . + +![Politics](https://github.com/user-attachments/assets/a2685618-be5f-4cc2-aec6-b96636914aae) + +![Travels](https://github.com/user-attachments/assets/00aa2883-488f-4057-a05d-f6e464fe51de) + +![Misteries](https://github.com/user-attachments/assets/70c06b1c-1d35-40cb-839f-9aee4e2c249d) + +![Any theme](https://github.com/user-attachments/assets/2e0e0e7d-03ad-4c82-a26c-5579fcca4fac) + +![admin view](https://github.com/user-attachments/assets/7d7b8f00-fe92-41e3-b86b-b1259ffbe519) -![image](https://github.com/user-attachments/assets/f01bf1a9-a2b9-4bfd-9995-2756219be659) -![image](https://github.com/user-attachments/assets/81b1e192-83a7-40f1-b856-b4f33f007f1c) # Getting started! 💥 🚀 ## Requirements ✅ +You need to have a NEWS API's API KEY to use this plugin. + +You can get one for free here : https://newsapi.org/docs + You need to have a GEMINI API KEY to use this plugin. You can get one for free here : https://ai.google.dev/gemini-api/docs/api-key

Next Excecution

Hook Name

Function Name

' . esc_html(gmdate('Y-m-d H:i:s', $timestamp)) . '

' . esc_html($hook) . '

' . esc_html(strval($function_parts['function'][1] ? $function_parts['function'][1] : $function_parts['function'][0])) . '