├── screenshots
└── screenshot3.png
├── LICENSE
├── ai-sports-writer.php
├── .gitignore
├── includes
├── Utilities
│ └── Logger.php
├── Autoloader.php
├── Admin
│ ├── CronSettingsPage.php
│ ├── ApiConfigPage.php
│ └── PostConfigPage.php
├── Services
│ ├── SportApiService.php
│ └── OpenAiService.php
└── Core
│ ├── Plugin.php
│ └── ContentGenerator.php
├── README.md
└── assets
└── js
└── ai-sports-writer.js
/screenshots/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithKola/AI-Sports-Writer/HEAD/screenshots/screenshot3.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | AI Sports Writer Plugin
2 | Copyright (C) 2024 Kolawole Yusuf
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 2 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU General Public License for more details.
13 |
14 | You should have received a copy of the GNU General Public License
15 | along with this program. If not, see
'; 35 | esc_html_e('To ensure cron jobs run on time, we recommend setting up server-side cron jobs. Below are the instructions:', 'ai-sports-writer'); 36 | ?>
37 |38 | * * * * * wget -q -O - 39 |40 |
41 |
42 | 43 | 44 | getCronHooks() as $hook => $config) { 46 | echo '' . esc_html($this->checkCronStatus($hook, $config['interval'], $config['offset'])) . '
'; 47 | } 48 | ?> 49 |' . esc_html__('Configure your API settings here. Enter the required keys and test API connectivity.', 'ai-sports-writer') . '
'; 93 | } 94 | 95 | /** 96 | * Sport API key input callback 97 | */ 98 | public function sport_api_key_callback(): void 99 | { 100 | $options = get_option('aisprtsw_api_settings'); 101 | $sport_api_key = $options['sport_api_key'] ?? ''; 102 | 103 | printf( 104 | ' 105 |%s
', 106 | esc_attr('aisprtsw_api_settings'), 107 | esc_attr($sport_api_key), 108 | esc_html__('Enter your API key from scalesp.com. You can get an API key by signing up or logging into your account on scalesp.com.', 'ai-sports-writer') 109 | ); 110 | } 111 | 112 | /** 113 | * OpenAI API key input callback 114 | */ 115 | public function openai_api_key_callback(): void 116 | { 117 | $options = get_option('aisprtsw_api_settings'); 118 | $openai_api_key = $options['openai_api_key'] ?? ''; 119 | 120 | printf( 121 | ' 122 |%s
', 123 | esc_attr('aisprtsw_api_settings'), 124 | esc_attr($openai_api_key), 125 | esc_html__('Enter your OpenAI API key. You can obtain an API key by signing up or logging into your account on openai.com.', 'ai-sports-writer') 126 | ); 127 | } 128 | 129 | /** 130 | * OpenAI model selection callback 131 | */ 132 | public function openai_model_callback(): void 133 | { 134 | $options = get_option('aisprtsw_api_settings'); 135 | $current_model = $options['openai_model'] ?? 'gpt-3.5-turbo'; 136 | 137 | // Build options array 138 | $model_options = array_map(function ($model_key, $model_name) use ($current_model) { 139 | return sprintf( 140 | '', 141 | esc_attr($model_key), 142 | selected($current_model, $model_key, false), 143 | esc_html($model_name) 144 | ); 145 | }, array_keys(self::ALLOWED_MODELS), self::ALLOWED_MODELS); 146 | 147 | $model_options = wp_kses( 148 | implode('', $model_options), 149 | [ 150 | 'option' => [ 151 | 'value' => [], 152 | 'selected' => [] 153 | ] 154 | ] 155 | ); 156 | 157 | printf( 158 | ' 159 |%s
', 160 | esc_attr('aisprtsw_api_settings'), 161 | $model_options, 162 | esc_html__('Select the OpenAI model to use for content generation.', 'ai-sports-writer') 163 | ); 164 | } 165 | 166 | /** 167 | * Test Sport API Ajax handler 168 | */ 169 | public function test_sport_api(): void 170 | { 171 | // Verify nonce for security 172 | check_ajax_referer('fcg_nonce', 'nonce'); 173 | 174 | // Get the API key from options 175 | $options = get_option('aisprtsw_api_settings'); 176 | $api_key = $options['sport_api_key'] ?? ''; 177 | 178 | // Check if the API key is provided 179 | if (empty($api_key)) { 180 | wp_send_json_error(['message' => __('API Key is missing', 'ai-sports-writer')]); 181 | return; 182 | } 183 | 184 | try { 185 | // Initialize the SportApiService 186 | $sport_api_service = new \AiSprtsW\Services\SportApiService(); 187 | 188 | // Fetch regions 189 | $regions = $sport_api_service->fetchFootballRegions($api_key); 190 | 191 | // Insert regions into database 192 | $this->insert_regions_into_db($regions); 193 | 194 | // Send success response 195 | wp_send_json_success(['message' => __('API Connection Successful', 'ai-sports-writer')]); 196 | } catch (\Exception $e) { 197 | // Log the error and send failure response 198 | Logger::log('Sport API Test Error: ' . $e->getMessage(), 'ERROR'); 199 | wp_send_json_error(['message' => sprintf( 200 | // translators: %s is the error message returned from the exception 201 | __('Request failed: %s', 'ai-sports-writer'), 202 | $e->getMessage() 203 | )]); 204 | } 205 | } 206 | 207 | /** 208 | * Insert regions into database 209 | * 210 | * @param array $regions Regions data to insert 211 | */ 212 | private function insert_regions_into_db(array $regions): void 213 | { 214 | global $wpdb; 215 | $regions = $regions['data'] ?? []; 216 | $table_name = esc_sql($wpdb->prefix . 'football_regions'); 217 | 218 | // Prepare and insert regions 219 | foreach ($regions as $region) { 220 | $name = sanitize_text_field($region['name']); 221 | $leagues = wp_json_encode($region['leagues']); 222 | 223 | // Check if region exists 224 | $existing_region = $wpdb->get_var( 225 | $wpdb->prepare( 226 | "SELECT COUNT(*) FROM {$table_name} WHERE name = %s", 227 | $name 228 | ) 229 | ); 230 | 231 | if ($existing_region) { 232 | continue; // Skip duplicate region 233 | } 234 | 235 | // Insert new region 236 | $wpdb->insert( 237 | $table_name, 238 | [ 239 | 'name' => $name, 240 | 'leagues' => $leagues, 241 | ], 242 | ['%s', '%s'] 243 | ); 244 | } 245 | } 246 | 247 | 248 | 249 | /** 250 | * Fetch regions via Ajax 251 | */ 252 | public function fetch_regions_ajax(): void 253 | { 254 | // Verify nonce 255 | check_ajax_referer('fcg_nonce', 'nonce'); 256 | 257 | global $wpdb; 258 | 259 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 260 | $regions_table = esc_sql($wpdb->prefix . 'football_regions'); 261 | 262 | // Fetch regions with selection status 263 | $regions = $wpdb->get_results( 264 | " 265 | SELECT r.*, 266 | (SELECT COUNT(*) FROM {$content_regions_table} cr WHERE cr.region_id = r.id) > 0 as selected 267 | FROM {$regions_table} r 268 | ", 269 | ARRAY_A 270 | ); 271 | 272 | wp_send_json_success($regions); 273 | } 274 | 275 | 276 | 277 | /** 278 | * Save content regions via Ajax 279 | */ 280 | public function save_content_regions_ajax(): void 281 | { 282 | // Verify nonce 283 | check_ajax_referer('fcg_nonce', 'nonce'); 284 | 285 | global $wpdb; 286 | 287 | // Sanitize the table name 288 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 289 | 290 | // Sanitize and validate selected regions 291 | $selected_regions = isset($_POST['selected_regions']) 292 | ? array_map('intval', (array)$_POST['selected_regions']) 293 | : []; 294 | 295 | if (empty($selected_regions)) { 296 | wp_send_json_error(['message' => __('No regions selected.', 'ai-sports-writer')]); 297 | return; 298 | } 299 | 300 | // Clear existing selections 301 | $wpdb->query("TRUNCATE TABLE {$content_regions_table}"); 302 | 303 | // Insert new selections 304 | foreach ($selected_regions as $region_id) { 305 | $wpdb->insert( 306 | $content_regions_table, 307 | ['region_id' => $region_id], 308 | ['%d'] 309 | ); 310 | } 311 | 312 | wp_send_json_success(['message' => __('Regions saved successfully.', 'ai-sports-writer')]); 313 | } 314 | 315 | 316 | /** 317 | * Sanitize and validate settings 318 | * 319 | * @param array $input Unsanitized input settings 320 | * @return array Sanitized settings 321 | */ 322 | public function sanitize_settings(array $input): array 323 | { 324 | $sanitized = []; 325 | 326 | // Sanitize API keys 327 | $sanitized['sport_api_key'] = sanitize_text_field($input['sport_api_key'] ?? ''); 328 | $sanitized['openai_api_key'] = sanitize_text_field($input['openai_api_key'] ?? ''); 329 | 330 | // Validate OpenAI model 331 | if (isset($input['openai_model']) && array_key_exists($input['openai_model'], self::ALLOWED_MODELS)) { 332 | $sanitized['openai_model'] = $input['openai_model']; 333 | } else { 334 | $sanitized['openai_model'] = 'gpt-3.5-turbo'; 335 | add_settings_error( 336 | 'aisprtsw_api_settings', 337 | 'invalid_openai_model', 338 | __('Invalid OpenAI model selected. Defaulting to GPT-3.5 Turbo.', 'ai-sports-writer') 339 | ); 340 | } 341 | 342 | return $sanitized; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /includes/Core/Plugin.php: -------------------------------------------------------------------------------- 1 | 0, 52 | 'warning' => 1, 53 | 'error' => 2 54 | ]; 55 | 56 | /** 57 | * Private constructor to prevent direct instantiation 58 | */ 59 | private function __construct() 60 | { 61 | $this->initializePages(); 62 | $this->setupHooks(); 63 | } 64 | 65 | /** 66 | * Singleton instance getter 67 | * 68 | * @return self 69 | */ 70 | public static function init() 71 | { 72 | if (self::$instance === null) { 73 | self::$instance = new self(); 74 | 75 | // Register activation and deactivation hooks 76 | register_activation_hook(AISPRTSW_PLUGIN_FILE, [self::$instance, 'activate']); 77 | register_deactivation_hook(AISPRTSW_PLUGIN_FILE, [self::$instance, 'deactivate']); 78 | add_action('init', [self::class, 'initialize_content_generator']); 79 | } 80 | return self::$instance; 81 | } 82 | 83 | /** 84 | * Plugin activation method 85 | */ 86 | public function activate(): void 87 | { 88 | // Ensure plugin is only activated by administrators 89 | if (!current_user_can('activate_plugins')) { 90 | return; 91 | } 92 | 93 | try { 94 | $this->create_sport_ai_writer_tables(); 95 | $this->log('Plugin activated successfully'); 96 | } catch (Exception $e) { 97 | $this->log("Activation failed: {$e->getMessage()}", 'error'); 98 | 99 | // Prevent plugin activation and show error 100 | 101 | wp_die( 102 | sprintf( 103 | // Translators: %s is the error message from an exception during plugin activation 104 | esc_html__('AI Sports Writer could not be activated. Error: %s', 'ai-sports-writer'), 105 | esc_html($e->getMessage()) 106 | ), 107 | esc_html__('Plugin Activation Error', 'ai-sports-writer'), 108 | ['response' => 500] 109 | ); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Plugin deactivation method 116 | */ 117 | public function deactivate(): void 118 | { 119 | // Restrict plugin deactivation to administrators only. 120 | if (!current_user_can('activate_plugins')) { 121 | return; 122 | } 123 | 124 | // Clear scheduled cron jobs 125 | wp_clear_scheduled_hook('aisprtsw_cron'); 126 | wp_clear_scheduled_hook('aisprtsw_fetch_cron'); 127 | 128 | $this->log('Plugin deactivated'); 129 | } 130 | 131 | /** 132 | * Initialize admin pages 133 | */ 134 | private function initializePages( 135 | PostConfigPage $postConfigPage = null, 136 | CronSettingsPage $cronSettingsPage = null, 137 | ApiConfigPage $apiConfigPage = null 138 | ): void { 139 | $this->postConfigPage = $postConfigPage ?? new PostConfigPage(); 140 | $this->cronSettingsPage = $cronSettingsPage ?? new CronSettingsPage(); 141 | $this->apiConfigPage = $apiConfigPage ?? new ApiConfigPage(); 142 | } 143 | 144 | /** 145 | * Setup WordPress hooks 146 | */ 147 | private function setupHooks(): void 148 | { 149 | add_action('init', [$this, 'registerPages']); 150 | add_action('admin_menu', [$this, 'addMainPluginMenu']); 151 | add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']); 152 | } 153 | 154 | /** 155 | * Enqueue admin scripts and styles 156 | */ 157 | public function enqueueAdminScripts($hook): void 158 | { 159 | // Only enqueue on plugin pages 160 | if (strpos($hook, 'ai-sports-writer') === false) { 161 | return; 162 | } 163 | 164 | wp_enqueue_media(); 165 | wp_enqueue_script( 166 | 'ai-sports-writer-js', 167 | plugin_dir_url(AISPRTSW_PLUGIN_FILE) . 'assets/js/ai-sports-writer.js', 168 | ['jquery'], 169 | self::PLUGIN_VERSION, 170 | true 171 | ); 172 | 173 | // Add localized variables to the JS file 174 | wp_localize_script( 175 | 'ai-sports-writer-js', 176 | 'fcg_ajax_object', 177 | [ 178 | 'ajax_url' => admin_url('admin-ajax.php'), 179 | 'nonce' => wp_create_nonce('fcg_nonce') 180 | ] 181 | ); 182 | } 183 | 184 | 185 | /** 186 | * Register admin pages 187 | */ 188 | public function registerPages(): void 189 | { 190 | $this->postConfigPage->register(); 191 | $this->cronSettingsPage->register(); 192 | $this->apiConfigPage->register(); 193 | } 194 | 195 | /** 196 | * Add main plugin menu and submenus 197 | */ 198 | public function addMainPluginMenu(): void 199 | { 200 | if (!current_user_can('manage_options')) { 201 | return; 202 | } 203 | 204 | add_menu_page( 205 | __('AI Sports Writer', 'ai-sports-writer'), 206 | __('AI Sports Writer', 'ai-sports-writer'), 207 | 'manage_options', 208 | self::MENU_SLUG, 209 | [$this, 'renderMainPage'], 210 | 'dashicons-admin-site-alt3', 211 | 20 212 | ); 213 | 214 | add_submenu_page( 215 | self::MENU_SLUG, 216 | __('Post Settings', 'ai-sports-writer'), 217 | __('Post Config', 'ai-sports-writer'), 218 | 'manage_options', 219 | self::POST_SETTINGS_SLUG, 220 | [$this, 'post_settings_page'] 221 | ); 222 | 223 | add_submenu_page( 224 | self::MENU_SLUG, 225 | __('Cron Settings', 'ai-sports-writer'), 226 | __('Cron status', 'ai-sports-writer'), 227 | 'manage_options', 228 | self::CRON_SETTINGS_SLUG, 229 | [$this, 'post_settings_cron'] 230 | ); 231 | } 232 | 233 | public static function initialize_content_generator() 234 | { 235 | $sport_api_service = new SportApiService(); 236 | $openai_service = new OpenAiService(); 237 | 238 | $content_generator = new ContentGenerator($sport_api_service, $openai_service); 239 | } 240 | 241 | /** 242 | * Render main plugin page 243 | */ 244 | public function renderMainPage(): void 245 | { 246 | ?> 247 |' . esc_html__('Configure post generation settings such as the maximum number of posts per day/hour.', 'ai-sports-writer') . '
'; 173 | } 174 | 175 | // Render field for maximum games per day 176 | public function render_max_games_per_day_field(): void 177 | { 178 | $options = get_option('aisprtsw_post_settings'); 179 | $value = $options['max_games_per_day'] ?? 5; 180 | echo ''; 181 | } 182 | 183 | // Render field for maximum games per hour 184 | public function render_max_games_per_hour_field(): void 185 | { 186 | $options = get_option('aisprtsw_post_settings'); 187 | $value = $options['max_games_per_hour'] ?? 5; 188 | echo ''; 189 | } 190 | 191 | // Render field for post intervals (in minutes) 192 | public function render_post_intervals_field(): void 193 | { 194 | $options = get_option('aisprtsw_post_settings'); 195 | $value = $options['post_intervals'] ?? 5; 196 | echo ''; 197 | echo 'Interval in minutes between scheduled posts (1-30 minutes).
'; 198 | } 199 | 200 | // Render field for default post author 201 | public function render_post_author_field(): void 202 | { 203 | $options = get_option('aisprtsw_post_settings'); 204 | $selected = $options['post_author'] ?? get_current_user_id(); 205 | 206 | $users = get_users([ 207 | 'capability' => 'publish_posts', 208 | 'fields' => ['ID', 'display_name'], 209 | ]); 210 | 211 | echo ''; 219 | echo 'Select the default author for automatically generated posts.
'; 220 | } 221 | 222 | 223 | 224 | // Render field for default post category 225 | public function render_post_category_field(): void 226 | { 227 | $options = get_option('aisprtsw_post_settings'); 228 | $selected = $options['post_category'] ?? 0; 229 | 230 | $categories = get_categories(['hide_empty' => false]); 231 | 232 | echo ''; 246 | echo 'Select the default category for automatically generated posts.
'; 247 | } 248 | 249 | 250 | public function render_prompt_settings_section(): void {} 251 | 252 | //Render field for AI content generation prompt 253 | public function render_ai_content_prompt_field(): void 254 | { 255 | $options = get_option('aisprtsw_post_settings'); 256 | $value = $options['ai_content_prompt'] ?? ''; 257 | 258 | echo ''; 259 | echo 'Customize the prompt sent to OpenAI for content generation.
'; 260 | } 261 | 262 | 263 | // Render callback for image settings 264 | public function render_image_settings_section(): void 265 | { 266 | echo 'Set the configuration for featured images in posts.
'; 267 | } 268 | // Render field for uploading featured image 269 | public function render_featured_image_upload_field(): void 270 | { 271 | $options = get_option('aisprtsw_post_settings'); 272 | $value = $options['featured_image_url'] ?? ''; 273 | 274 | echo ''; 275 | echo ''; 276 | 277 | $attachment_id = attachment_url_to_postid($value); 278 | 279 | if ($attachment_id) { 280 | // Display the image using wp_get_attachment_image 281 | echo 'When checked, the plugin will attempt to generate a featured image using DALL-E for each post.
'; 298 | } 299 | 300 | // Render dropdown for DALL-E image size 301 | public function render_dalle_image_size_field(): void 302 | { 303 | $options = get_option('aisprtsw_post_settings'); 304 | $current_size = $options['dalle_image_size'] ?? '1024x1024'; 305 | 306 | $size_options = [ 307 | '1024x1024' => 'Square (1024x1024) - Classic format', 308 | '1792x1024' => 'Landscape (1792x1024) - Stadium scenes', 309 | '1024x1792' => 'Portrait (1024x1792) - Social media' 310 | ]; 311 | 312 | echo ''; 323 | echo 'Choose the aspect ratio that best fits your content layout.
'; 324 | } 325 | 326 | // Render dropdown for DALL-E image quality 327 | public function render_dalle_image_quality_field(): void 328 | { 329 | $options = get_option('aisprtsw_post_settings'); 330 | $current_quality = $options['dalle_image_quality'] ?? 'standard'; 331 | 332 | $quality_options = [ 333 | 'standard' => 'Standard - Faster generation, lower cost', 334 | 'hd' => 'HD - Higher detail, premium quality' 335 | ]; 336 | 337 | echo ''; 348 | echo 'HD quality provides more detailed images but costs more to generate.
'; 349 | } 350 | 351 | 352 | // Sanitize input settings before saving 353 | public function sanitize_settings($input): array 354 | { 355 | $sanitized = []; 356 | 357 | // Max games per day validation 358 | if ($input['max_games_per_day'] < 1 || $input['max_games_per_day'] > 100) { 359 | add_settings_error( 360 | 'aisprtsw_post_settings', 361 | 'invalid_max_games_per_day', 362 | __('Maximum games per day should be between 1 and 100.', 'ai-sports-writer') 363 | ); 364 | $sanitized['max_games_per_day'] = 5; 365 | } else { 366 | $sanitized['max_games_per_day'] = (int) $input['max_games_per_day']; 367 | } 368 | 369 | 370 | // Max games per hour validation 371 | if ($input['max_games_per_hour'] < 1 || $input['max_games_per_hour'] > 24) { 372 | add_settings_error( 373 | 'aisprtsw_post_settings', 374 | 'invalid_max_games_per_hour', 375 | __('Maximum games per hour should be between 1 and 24.', 'ai-sports-writer') 376 | ); 377 | $sanitized['max_games_per_hour'] = 5; 378 | } else { 379 | $sanitized['max_games_per_hour'] = (int) $input['max_games_per_hour']; 380 | } 381 | 382 | // Post intervals validation 383 | $post_intervals = (int) ($input['post_intervals'] ?? 5); 384 | if ($post_intervals < 1 || $post_intervals > 30) { 385 | add_settings_error( 386 | 'aisprtsw_post_settings', 387 | 'invalid_post_intervals', 388 | __('Post intervals should be between 1 and 30 minutes.', 'ai-sports-writer') 389 | ); 390 | $sanitized['post_intervals'] = 5; 391 | } else { 392 | $sanitized['post_intervals'] = $post_intervals; 393 | } 394 | 395 | // Featured image url validator 396 | $featured_image_url = esc_url_raw($input['featured_image_url'] ?? ''); 397 | if (!empty($featured_image_url)) { 398 | $file_type = wp_check_filetype($featured_image_url); 399 | $allowed_image_types = ['jpg', 'jpeg', 'png', 'gif']; 400 | 401 | if (!filter_var($featured_image_url, FILTER_VALIDATE_URL)) { 402 | add_settings_error( 403 | 'aisprtsw_post_settings', 404 | 'invalid_featured_image_url', 405 | __('The featured image URL is not a valid URL.', 'ai-sports-writer') 406 | ); 407 | $sanitized['featured_image_url'] = ''; 408 | } elseif (!in_array($file_type['ext'], $allowed_image_types)) { 409 | add_settings_error( 410 | 'aisprtsw_post_settings', 411 | 'invalid_featured_image_type', 412 | __('The featured image URL does not point to a valid image file (jpg, jpeg, png, or gif).', 'ai-sports-writer') 413 | ); 414 | $sanitized['featured_image_url'] = ''; 415 | } else { 416 | $sanitized['featured_image_url'] = $featured_image_url; 417 | } 418 | } else { 419 | $sanitized['featured_image_url'] = ''; 420 | } 421 | 422 | $sanitized['dalle_image_generation'] = (int) ($input['dalle_image_generation'] ?? 0); 423 | 424 | // DALL-E image size validation 425 | $allowed_sizes = ['1024x1024', '1792x1024', '1024x1792']; 426 | $sanitized['dalle_image_size'] = in_array($input['dalle_image_size'] ?? '', $allowed_sizes, true) 427 | ? $input['dalle_image_size'] 428 | : '1024x1024'; 429 | 430 | // DALL-E image quality validation 431 | $allowed_qualities = ['standard', 'hd']; 432 | $sanitized['dalle_image_quality'] = in_array($input['dalle_image_quality'] ?? '', $allowed_qualities, true) 433 | ? $input['dalle_image_quality'] 434 | : 'standard'; 435 | 436 | $sanitized['post_author'] = (int) ($input['post_author'] ?? get_current_user_id()); 437 | $sanitized['post_category'] = (int) ($input['post_category'] ?? 0); 438 | 439 | $allowed_html = [ 440 | 'p' => [], 441 | 'br' => [], 442 | 'strong' => [], 443 | 'em' => [], 444 | 'ul' => [], 445 | 'ol' => [], 446 | 'li' => [], 447 | ]; 448 | 449 | $sanitized['ai_content_prompt'] = wp_kses($input['ai_content_prompt'] ?? '', $allowed_html); 450 | $allowed_models = [ 451 | 'gpt-3.5-turbo', 452 | 'gpt-4o-mini', 453 | 'gpt-4-turbo', 454 | 'gpt-4o', 455 | 'gpt-4o-2024-08-06', 456 | 'o1-mini' 457 | ]; 458 | $sanitized['openai_model'] = in_array($input['openai_model'] ?? '', $allowed_models, true) 459 | ? $input['openai_model'] 460 | : 'gpt-3.5-turbo'; 461 | 462 | 463 | return $sanitized; 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /includes/Core/ContentGenerator.php: -------------------------------------------------------------------------------- 1 | sport_api_service = $sport_api_service; 20 | $this->openai_service = $openai_service; 21 | $this->setup_cron(); 22 | } 23 | 24 | /** 25 | * Set up cron jobs for content generation. 26 | */ 27 | private function setup_cron(): void 28 | { 29 | add_filter('cron_schedules', [$this, 'register_custom_intervals']); 30 | 31 | // Schedule events if not already scheduled 32 | if (!wp_next_scheduled('aisprtsw_fetch_cron')) { 33 | wp_schedule_event(time(), 'every_three_hours', 'aisprtsw_fetch_cron'); 34 | } 35 | if (!wp_next_scheduled('aisprtsw_cron')) { 36 | wp_schedule_event(time(), 'ten_minutes_before_hour', 'aisprtsw_cron'); 37 | } 38 | 39 | add_action('aisprtsw_fetch_cron', array($this, 'run_upcoming_games')); 40 | add_action('aisprtsw_cron', array($this, 'run_content_generation')); 41 | } 42 | 43 | /** 44 | * Register custom cron intervals. 45 | * 46 | * @param array $schedules Existing cron schedules. 47 | * @return array Modified cron schedules. 48 | */ 49 | public function register_custom_intervals(array $schedules): array 50 | { 51 | $schedules['every_three_hours'] = [ 52 | 'interval' => 3 * HOUR_IN_SECONDS, 53 | 'display' => __('Every 3 Hours', 'ai-sports-writer') 54 | ]; 55 | $schedules['ten_minutes_before_hour'] = [ 56 | 'interval' => 3600 - 600, // 1 hour - 10 minutes 57 | 'display' => __('10 Minutes Before Hour', 'ai-sports-writer') 58 | ]; 59 | return $schedules; 60 | } 61 | 62 | 63 | public function run_upcoming_games(): void 64 | { 65 | $api_options = get_option(self::OPTION_API_NAME); 66 | $api_key = $api_options['sport_api_key'] ?? ''; 67 | 68 | if (empty($api_key)) { 69 | Logger::log('Sport API key not found.', 'error'); 70 | return; 71 | } 72 | 73 | $games = $this->sport_api_service->fetchUpcomingEndpoint($api_key); 74 | if ($games) { 75 | $this->insert_games_into_db($games); 76 | } else { 77 | Logger::log('Failed to fetch games data.', 'error'); 78 | } 79 | } 80 | 81 | 82 | 83 | private function insert_games_into_db(array $games): void 84 | { 85 | global $wpdb; 86 | $table_name = $wpdb->prefix . 'football_games'; 87 | $two_days_ago = gmdate('Y-m-d H:i:s', strtotime('-2 days')); 88 | 89 | //deleting old games 90 | $where = [ 91 | 'match_datetime' => $two_days_ago 92 | ]; 93 | $deleted_count = $wpdb->delete($table_name, $where, ['%s']); 94 | 95 | $inserted_count = 0; 96 | foreach ($games['data'] as $game) { 97 | // Check for existing match 98 | $existing_match = $wpdb->get_var( 99 | $wpdb->prepare( 100 | "SELECT COUNT(*) FROM {$table_name} WHERE match_code = %s", 101 | $game['match_code'] 102 | ) 103 | ); 104 | 105 | if ($existing_match > 0) { 106 | continue; 107 | } 108 | 109 | // Prepare data for insertion 110 | $data = [ 111 | 'match_code' => $game['match_code'] ?? null, 112 | 'region' => $game['region'] ?? '', 113 | 'team' => $game['team'] ?? '', 114 | 'home' => $game['home'] ?? '', 115 | 'away' => $game['away'] ?? '', 116 | 'match_datetime' => $game['match_datetime'] ?? null, 117 | 'time_zone' => $game['time_zone'] ?? '', 118 | 'provider' => $game['provider'] ?? '', 119 | 'odds' => wp_json_encode($game['odds'] ?? []), 120 | ]; 121 | 122 | // Formats for each field to ensure proper sanitization 123 | $formats = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']; 124 | 125 | // Insert the game data 126 | $result = $wpdb->insert($table_name, $data, $formats); 127 | if ($result) { 128 | $inserted_count++; 129 | } 130 | } 131 | 132 | Logger::log("Games processed: " . count($games['data']) . ", inserted: $inserted_count"); 133 | } 134 | 135 | 136 | 137 | 138 | 139 | public function run_content_generation(): void 140 | { 141 | $api_options = get_option(self::OPTION_API_NAME); 142 | $post_options = get_option(self::OPTION_POST_NAME); 143 | 144 | 145 | $openai_api_key = $api_options['openai_api_key'] ?? ''; 146 | $sport_api_key = $api_options['sport_api_key'] ?? ''; 147 | 148 | if (empty($openai_api_key) || empty($sport_api_key)) { 149 | Logger::log('API keys not found.', 'error'); 150 | return; 151 | } 152 | 153 | 154 | $max_games_per_day = (int)($post_options['max_games_per_day'] ?? 5); 155 | $max_games_per_hour = (int)($post_options['max_games_per_hour'] ?? 5); 156 | $post_interval = (int)($post_options['post_intervals'] ?? 5); 157 | $ai_content_prompt = $post_options['ai_content_prompt'] ?? ''; 158 | 159 | global $wpdb; 160 | $table_name = $wpdb->prefix . 'football_games'; 161 | $today = gmdate('Y-m-d'); 162 | 163 | 164 | $games_processed_today = $wpdb->get_var( 165 | $wpdb->prepare( 166 | "SELECT COUNT(*) FROM `{$wpdb->prefix}football_games` WHERE processed = 1 AND DATE(processed_started_at) = %s", 167 | $today 168 | ) 169 | ); 170 | 171 | if ($games_processed_today >= $max_games_per_day) { 172 | Logger::log("Max games per day reached ($games_processed_today).", 'warning'); 173 | return; 174 | } 175 | 176 | 177 | 178 | $current_time = current_time('mysql'); 179 | $games = $wpdb->get_results( 180 | $wpdb->prepare( 181 | "SELECT * FROM {$wpdb->prefix}football_games WHERE processed = 0 AND match_datetime > %s LIMIT %d", 182 | $current_time, 183 | $max_games_per_hour 184 | ), 185 | ARRAY_A 186 | ); 187 | 188 | if (!$games) { 189 | Logger::log('No unprocessed games found.', 'notice'); 190 | return; 191 | } 192 | 193 | 194 | foreach ($games as $index => $game) { 195 | try { 196 | $wpdb->update( 197 | $table_name, 198 | ['processed' => 1, 'processed_started_at' => current_time('mysql')], 199 | ['id' => $game['id']] 200 | ); 201 | 202 | $all_stats = $this->sport_api_service->fetchGameStatistics($sport_api_key, $game['match_code']); 203 | 204 | if ($all_stats === null) { 205 | // Handle error, e.g., log, and skip to the next game. 206 | Logger::log('Failed to fetch stats for match code: ' . $game['match_code'] . '.', 'error'); 207 | 208 | // Mark game processing as failed or skipped if needed. 209 | $wpdb->update( 210 | $table_name, 211 | ['processed' => 2, 'processed_failed_at' => current_time('mysql')], 212 | ['id' => $game['id']] 213 | ); 214 | continue; 215 | } 216 | 217 | 218 | $prompt = $this->prepare_content_prompt($game, $all_stats, $ai_content_prompt); 219 | $ai_content = $this->openai_service->generateContent($openai_api_key, $prompt); 220 | 221 | $post_time = gmdate('Y-m-d H:i:s', strtotime("+1 hour +" . ($index * $post_interval) . " minutes")); 222 | 223 | if ($ai_content) { 224 | $this->schedule_content_post($ai_content, $post_time, $game); 225 | } else { 226 | Logger::log('Failed to generate content.', 'error'); 227 | } 228 | $wpdb->update($table_name, ['process_completed_at' => current_time('mysql')], ['id' => $game['id']]); 229 | } catch (\Throwable $th) { 230 | Logger::log("An error occurred during game processing: " . $th->getMessage(), 'error'); 231 | 232 | continue; // Skip to the next game 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Prepare content prompt with game details 239 | * 240 | * @param array $game Game data 241 | * @param array $all_stats Game statistics 242 | * @param string $base_prompt Initial content prompt 243 | * @return string Prepared content prompt 244 | */ 245 | private function prepare_content_prompt( 246 | array $game, 247 | array $all_stats, 248 | string $base_prompt 249 | ): string { 250 | 251 | $home_recent_matches = ""; 252 | $away_recent_matches = ""; 253 | $head_to_head_matches = ""; 254 | 255 | if (isset($all_stats["home_matches"]) && is_array($all_stats["home_matches"])) { 256 | $home_recent_matches = implode( 257 | "\n", 258 | array_map( 259 | function ($match) { 260 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 261 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 262 | : ""; 263 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 264 | $halfTime . 265 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 266 | ". Date: " . $match["match_date"]; 267 | }, 268 | array_reverse($all_stats["home_matches"]) //display recent history first 269 | ) 270 | ); 271 | } 272 | 273 | 274 | 275 | 276 | if (isset($all_stats["away_matches"]) && is_array($all_stats["away_matches"])) { 277 | $away_recent_matches = implode( 278 | "\n", 279 | array_map( 280 | function ($match) { 281 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 282 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 283 | : ""; 284 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 285 | $halfTime . 286 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 287 | ". Date: " . $match["match_date"]; 288 | }, 289 | array_reverse($all_stats["away_matches"]) //display recent history first 290 | ) 291 | ); 292 | } 293 | 294 | 295 | 296 | if (isset($all_stats["head_to_head"]) && is_array($all_stats["head_to_head"])) { 297 | $head_to_head_matches = implode( 298 | "\n", 299 | array_map( 300 | function ($match) { 301 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 302 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 303 | : ""; 304 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 305 | $halfTime . 306 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 307 | ". Date: " . $match["match_date"]; 308 | }, 309 | array_reverse($all_stats["head_to_head"]) //display recent history first 310 | ) 311 | ); 312 | } 313 | 314 | 315 | 316 | 317 | $oddsBreakdown = ""; 318 | $game["odds"] = json_decode($game["odds"], true); 319 | 320 | if (!empty($game["odds"])) { 321 | $oddsBreakdown = implode("\n", [ 322 | "\nBetting Odds Breakdown:", 323 | "- Home Win ({$game["home"]}): {$game["odds"]["1"]}", 324 | "- Away Win ({$game["away"]}): {$game["odds"]["2"]}", 325 | "- Either team to Win: {$game["odds"]["12"]}", 326 | "- Draw: {$game["odds"]["x"]}", 327 | "- Home win or draw: {$game["odds"]["1x"]}", 328 | "- Away win or draw: {$game["odds"]["x2"]}", 329 | "- Total goals, less than 3 goals: {$game["odds"]["u_2_5"]}", 330 | "- Total goals, 3 goals or more: {$game["odds"]["o_2_5"]}", 331 | ]); 332 | } 333 | 334 | 335 | $sections = [ 336 | "\n\nMatch Details:", 337 | "- Upcoming Match: {$game["home"]} vs {$game["away"]}", 338 | "- Home team: {$game["home"]}", 339 | "- Away team: {$game["away"]}", 340 | "- Match Date: {$game["match_datetime"]}", 341 | "- Region: {$game["region"]}", 342 | "", 343 | $oddsBreakdown, 344 | "", 345 | !empty($home_recent_matches) ? "\nMatch History Analysis:\nHome Team Recent Performance:\n$home_recent_matches" : null, 346 | !empty($away_recent_matches) ? "\nAway Team Recent Performance:\n$away_recent_matches" : null, 347 | !empty($head_to_head_matches) ? "\nHead-to-Head History:\n$head_to_head_matches" : null, 348 | ]; 349 | 350 | // Remove null or empty values 351 | $sections = array_filter($sections); 352 | 353 | // Create the final prompt 354 | $base_prompt .= implode("\n", $sections); 355 | 356 | return $base_prompt; 357 | } 358 | 359 | /** 360 | * Schedules a post with content, title, and featured image, and assigns it a future publish date. 361 | * 362 | * @param string $content The content of the post to be scheduled. 363 | * @param string $post_time The scheduled time for the post to be published. 364 | * @param object $game Game data that may be used to generate a DALL-E image. 365 | * 366 | * @return int|void The post ID of the created post or void if an error occurs. 367 | */ 368 | private function schedule_content_post($content, $post_time, $game) 369 | { 370 | 371 | $api_options = get_option(self::OPTION_API_NAME); 372 | $post_options = get_option(self::OPTION_POST_NAME); 373 | 374 | // Validate API keys 375 | $openai_api_key = $api_options['openai_api_key'] ?? ''; 376 | $sport_api_key = $api_options['sport_api_key'] ?? ''; 377 | 378 | if (empty($openai_api_key) || empty($sport_api_key)) { 379 | Logger::log('Missing API keys for content generation', 'ERROR'); 380 | return; 381 | } 382 | 383 | $openai_title = $this->openai_service->generateTitle($content, $openai_api_key); 384 | 385 | // If OpenAI title is empty or returns an error, we use the fallback method 386 | $post_title = !empty($openai_title) ? $openai_title : $this->generate_post_title($content); 387 | 388 | $post_data = [ 389 | 'post_title' => $post_title, 390 | 'post_content' => $content, 391 | 'post_status' => 'future', 392 | 'post_date' => $post_time, 393 | 'post_type' => 'post', 394 | 'post_author' => isset($post_options['post_author']) ? $post_options['post_author'] : get_current_user_id(), 395 | ]; 396 | 397 | $post_id = wp_insert_post($post_data); 398 | if (is_wp_error($post_id)) { 399 | Logger::log('Failed to insert post', 'ERROR'); 400 | return; 401 | } 402 | 403 | // Log the successful post creation 404 | // Logger::log("Post created successfully: ID {$post_id}", 'INFO'); 405 | 406 | // Handle categories 407 | if (isset($post_options['post_category']) && $post_options['post_category'] > 0) { 408 | wp_set_post_categories($post_id, [$post_options['post_category']], false); 409 | } 410 | 411 | // Handle featured image 412 | if ($post_id) { 413 | $manual_image = isset($post_options['featured_image_url']) ? $post_options['featured_image_url'] : ''; 414 | $dalle_enabled = isset($post_options['dalle_image_generation']) && $post_options['dalle_image_generation'] == 1; 415 | 416 | $featured_image_url = $dalle_image = null; 417 | if (!empty($manual_image)) { 418 | $featured_image_url = $manual_image; 419 | } 420 | 421 | if ($dalle_enabled) { 422 | $dalle_image = $this->openai_service->generateDalleImage($openai_api_key, $game); 423 | if ($dalle_image) { 424 | $featured_image_url = $dalle_image; 425 | } 426 | } 427 | 428 | // Upload and set featured image if URL is available 429 | if ($featured_image_url) { 430 | require_once ABSPATH . 'wp-admin/includes/file.php'; 431 | require_once ABSPATH . 'wp-admin/includes/media.php'; 432 | require_once ABSPATH . 'wp-admin/includes/image.php'; 433 | 434 | // Download the image from the URL 435 | $tmp_file = download_url($featured_image_url, 60); 436 | 437 | if (is_wp_error($tmp_file)) { 438 | Logger::log("Failed to download image from URL: {$featured_image_url}. Error: " . $tmp_file->get_error_message(), 'ERROR'); 439 | return; 440 | } 441 | 442 | // Get the mime type of the downloaded file 443 | $file_type = wp_check_filetype($tmp_file, null); 444 | 445 | // Note: DALL-E responses currently don't include file extensions in their metadata, 446 | // so we manually append .png as fallback if no extension is provided 447 | // Generate random filename with the correct extension 448 | $random_filename = 'image-' . uniqid() . (empty($file_type['ext']) ? '.png' : '.' . $file_type['ext']); 449 | 450 | 451 | // Prepare the file array for media_handle_sideload 452 | $file = [ 453 | 'name' => $random_filename, 454 | 'tmp_name' => $tmp_file, 455 | ]; 456 | 457 | // Use media_handle_sideload to upload the file 458 | $attachment_id = media_handle_sideload($file, $post_id); 459 | 460 | // Check for upload errors 461 | if (is_wp_error($attachment_id)) { 462 | wp_delete_file($tmp_file); // Remove the temporary file 463 | Logger::log("Failed to upload and sideload image. Error: " . $attachment_id->get_error_message(), 'ERROR'); 464 | return; 465 | } 466 | 467 | // Set the uploaded image as the featured image 468 | set_post_thumbnail($post_id, $attachment_id); 469 | 470 | // Log the successful image upload 471 | Logger::log("Featured image set successfully for post ID {$post_id}", 'INFO'); 472 | } 473 | } 474 | 475 | return $post_id; 476 | } 477 | 478 | 479 | 480 | /** 481 | * Generate a basic post title from the given content. 482 | * 483 | * @param string $content The content from which to generate the title. 484 | * @return string The generated post title. 485 | */ 486 | private function generate_post_title($content) 487 | { 488 | // Basic title generation from content 489 | $words = wp_trim_words($content, 6, '...'); 490 | return $words; 491 | } 492 | } 493 | --------------------------------------------------------------------------------