├── .gitignore
├── LICENSE
├── README.md
├── ai-sports-writer.php
├── assets
└── js
│ └── ai-sports-writer.js
├── includes
├── Admin
│ ├── ApiConfigPage.php
│ ├── CronSettingsPage.php
│ └── PostConfigPage.php
├── Autoloader.php
├── Core
│ ├── ContentGenerator.php
│ └── Plugin.php
├── Services
│ ├── OpenAiService.php
│ └── SportApiService.php
└── Utilities
│ └── Logger.php
└── screenshots
└── screenshot3.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Wordpress - ignore core, configuration, examples, uploads and logs.
2 | # https://github.com/github/gitignore/blob/main/WordPress.gitignore
3 |
4 | # Core
5 | #
6 | # Note: if you want to stage/commit WP core files
7 | # you can delete this whole section/until Configuration.
8 | /wp-admin/
9 | /wp-content/index.php
10 | /wp-content/languages
11 | /wp-content/plugins/index.php
12 | /wp-content/themes/index.php
13 | /wp-includes/
14 | /index.php
15 | /license.txt
16 | /readme.html
17 | /wp-*.php
18 | /xmlrpc.php
19 |
20 | # Configuration
21 | wp-config.php
22 |
23 | # Example themes
24 | /wp-content/themes/twenty*/
25 |
26 | # Example plugin
27 | /wp-content/plugins/hello.php
28 |
29 | # Uploads
30 | /wp-content/uploads/
31 |
32 | # Log files
33 | *.log
34 |
35 | # htaccess
36 | /.htaccess
37 |
38 | # All plugins
39 | #
40 | # Note: If you wish to whitelist plugins,
41 | # uncomment the next line
42 | #/wp-content/plugins
43 |
44 | # All themes
45 | #
46 | # Note: If you wish to whitelist themes,
47 | # uncomment the next line
48 | #/wp-content/themes
--------------------------------------------------------------------------------
/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
' . esc_html__('Configure your API settings here. Enter the required keys and test API connectivity.', 'ai-sports-writer') . '
'; 92 | } 93 | 94 | /** 95 | * Sport API key input callback 96 | */ 97 | public function sport_api_key_callback(): void 98 | { 99 | $options = get_option('aisprtsw_api_settings'); 100 | $sport_api_key = $options['sport_api_key'] ?? ''; 101 | 102 | printf( 103 | ' 104 |%s
', 105 | esc_attr('aisprtsw_api_settings'), 106 | esc_attr($sport_api_key), 107 | 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') 108 | ); 109 | } 110 | 111 | /** 112 | * OpenAI API key input callback 113 | */ 114 | public function openai_api_key_callback(): void 115 | { 116 | $options = get_option('aisprtsw_api_settings'); 117 | $openai_api_key = $options['openai_api_key'] ?? ''; 118 | 119 | printf( 120 | ' 121 |%s
', 122 | esc_attr('aisprtsw_api_settings'), 123 | esc_attr($openai_api_key), 124 | 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') 125 | ); 126 | } 127 | 128 | /** 129 | * OpenAI model selection callback 130 | */ 131 | public function openai_model_callback(): void 132 | { 133 | $options = get_option('aisprtsw_api_settings'); 134 | $current_model = $options['openai_model'] ?? 'gpt-3.5-turbo'; 135 | 136 | // Build options array 137 | $model_options = array_map(function ($model_key, $model_name) use ($current_model) { 138 | return sprintf( 139 | '', 140 | esc_attr($model_key), 141 | selected($current_model, $model_key, false), 142 | esc_html($model_name) 143 | ); 144 | }, array_keys(self::ALLOWED_MODELS), self::ALLOWED_MODELS); 145 | 146 | $model_options = wp_kses( 147 | implode('', $model_options), 148 | [ 149 | 'option' => [ 150 | 'value' => [], 151 | 'selected' => [] 152 | ] 153 | ] 154 | ); 155 | 156 | printf( 157 | ' 158 |%s
', 159 | esc_attr('aisprtsw_api_settings'), 160 | $model_options, 161 | esc_html__('Select the OpenAI model to use for content generation.', 'ai-sports-writer') 162 | ); 163 | } 164 | 165 | /** 166 | * Test Sport API Ajax handler 167 | */ 168 | public function test_sport_api(): void 169 | { 170 | // Verify nonce for security 171 | check_ajax_referer('fcg_nonce', 'nonce'); 172 | 173 | // Get the API key from options 174 | $options = get_option('aisprtsw_api_settings'); 175 | $api_key = $options['sport_api_key'] ?? ''; 176 | 177 | // Check if the API key is provided 178 | if (empty($api_key)) { 179 | wp_send_json_error(['message' => __('API Key is missing', 'ai-sports-writer')]); 180 | return; 181 | } 182 | 183 | try { 184 | // Initialize the SportApiService 185 | $sport_api_service = new \AiSprtsW\Services\SportApiService(); 186 | 187 | // Fetch regions 188 | $regions = $sport_api_service->fetchFootballRegions($api_key); 189 | 190 | // Insert regions into database 191 | $this->insert_regions_into_db($regions); 192 | 193 | // Send success response 194 | wp_send_json_success(['message' => __('API Connection Successful', 'ai-sports-writer')]); 195 | } catch (\Exception $e) { 196 | // Log the error and send failure response 197 | Logger::log('Sport API Test Error: ' . $e->getMessage(), 'ERROR'); 198 | wp_send_json_error(['message' => sprintf( 199 | // translators: %s is the error message returned from the exception 200 | __('Request failed: %s', 'ai-sports-writer'), 201 | $e->getMessage() 202 | )]); 203 | } 204 | } 205 | 206 | /** 207 | * Insert regions into database 208 | * 209 | * @param array $regions Regions data to insert 210 | */ 211 | private function insert_regions_into_db(array $regions): void 212 | { 213 | global $wpdb; 214 | $regions = $regions['data'] ?? []; 215 | $table_name = esc_sql($wpdb->prefix . 'football_regions'); 216 | 217 | // Prepare and insert regions 218 | foreach ($regions as $region) { 219 | $name = sanitize_text_field($region['name']); 220 | $leagues = wp_json_encode($region['leagues']); 221 | 222 | // Check if region exists 223 | $existing_region = $wpdb->get_var( 224 | $wpdb->prepare( 225 | "SELECT COUNT(*) FROM {$table_name} WHERE name = %s", 226 | $name 227 | ) 228 | ); 229 | 230 | if ($existing_region) { 231 | continue; // Skip duplicate region 232 | } 233 | 234 | // Insert new region 235 | $wpdb->insert( 236 | $table_name, 237 | [ 238 | 'name' => $name, 239 | 'leagues' => $leagues, 240 | ], 241 | ['%s', '%s'] 242 | ); 243 | } 244 | } 245 | 246 | 247 | 248 | /** 249 | * Fetch regions via Ajax 250 | */ 251 | public function fetch_regions_ajax(): void 252 | { 253 | // Verify nonce 254 | check_ajax_referer('fcg_nonce', 'nonce'); 255 | 256 | global $wpdb; 257 | 258 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 259 | $regions_table = esc_sql($wpdb->prefix . 'football_regions'); 260 | 261 | // Fetch regions with selection status 262 | $regions = $wpdb->get_results( 263 | " 264 | SELECT r.*, 265 | (SELECT COUNT(*) FROM {$content_regions_table} cr WHERE cr.region_id = r.id) > 0 as selected 266 | FROM {$regions_table} r 267 | ", 268 | ARRAY_A 269 | ); 270 | 271 | wp_send_json_success($regions); 272 | } 273 | 274 | 275 | 276 | /** 277 | * Save content regions via Ajax 278 | */ 279 | public function save_content_regions_ajax(): void 280 | { 281 | // Verify nonce 282 | check_ajax_referer('fcg_nonce', 'nonce'); 283 | 284 | global $wpdb; 285 | 286 | // Sanitize the table name 287 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 288 | 289 | // Sanitize and validate selected regions 290 | $selected_regions = isset($_POST['selected_regions']) 291 | ? array_map('intval', (array)$_POST['selected_regions']) 292 | : []; 293 | 294 | if (empty($selected_regions)) { 295 | wp_send_json_error(['message' => __('No regions selected.', 'ai-sports-writer')]); 296 | return; 297 | } 298 | 299 | // Clear existing selections 300 | $wpdb->query("TRUNCATE TABLE {$content_regions_table}"); 301 | 302 | // Insert new selections 303 | foreach ($selected_regions as $region_id) { 304 | $wpdb->insert( 305 | $content_regions_table, 306 | ['region_id' => $region_id], 307 | ['%d'] 308 | ); 309 | } 310 | 311 | wp_send_json_success(['message' => __('Regions saved successfully.', 'ai-sports-writer')]); 312 | } 313 | 314 | 315 | /** 316 | * Sanitize and validate settings 317 | * 318 | * @param array $input Unsanitized input settings 319 | * @return array Sanitized settings 320 | */ 321 | public function sanitize_settings(array $input): array 322 | { 323 | $sanitized = []; 324 | 325 | // Sanitize API keys 326 | $sanitized['sport_api_key'] = sanitize_text_field($input['sport_api_key'] ?? ''); 327 | $sanitized['openai_api_key'] = sanitize_text_field($input['openai_api_key'] ?? ''); 328 | 329 | // Validate OpenAI model 330 | if (isset($input['openai_model']) && array_key_exists($input['openai_model'], self::ALLOWED_MODELS)) { 331 | $sanitized['openai_model'] = $input['openai_model']; 332 | } else { 333 | $sanitized['openai_model'] = 'gpt-3.5-turbo'; 334 | add_settings_error( 335 | 'aisprtsw_api_settings', 336 | 'invalid_openai_model', 337 | __('Invalid OpenAI model selected. Defaulting to GPT-3.5 Turbo.', 'ai-sports-writer') 338 | ); 339 | } 340 | 341 | return $sanitized; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /includes/Admin/CronSettingsPage.php: -------------------------------------------------------------------------------- 1 | 29 |'; 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 post generation settings such as the maximum number of posts per day/hour.', 'ai-sports-writer') . '
'; 155 | } 156 | 157 | // Render field for maximum games per day 158 | public function render_max_games_per_day_field(): void 159 | { 160 | $options = get_option('aisprtsw_post_settings'); 161 | $value = $options['max_games_per_day'] ?? 5; 162 | echo ''; 163 | } 164 | 165 | // Render field for maximum games per hour 166 | public function render_max_games_per_hour_field(): void 167 | { 168 | $options = get_option('aisprtsw_post_settings'); 169 | $value = $options['max_games_per_hour'] ?? 5; 170 | echo ''; 171 | } 172 | 173 | // Render field for post intervals (in minutes) 174 | public function render_post_intervals_field(): void 175 | { 176 | $options = get_option('aisprtsw_post_settings'); 177 | $value = $options['post_intervals'] ?? 5; 178 | echo ''; 179 | echo 'Interval in minutes between scheduled posts (1-30 minutes).
'; 180 | } 181 | 182 | // Render field for default post author 183 | public function render_post_author_field(): void 184 | { 185 | $options = get_option('aisprtsw_post_settings'); 186 | $selected = $options['post_author'] ?? get_current_user_id(); 187 | 188 | $users = get_users([ 189 | 'capability' => 'publish_posts', 190 | 'fields' => ['ID', 'display_name'], 191 | ]); 192 | 193 | echo ''; 201 | echo 'Select the default author for automatically generated posts.
'; 202 | } 203 | 204 | 205 | 206 | // Render field for default post category 207 | public function render_post_category_field(): void 208 | { 209 | $options = get_option('aisprtsw_post_settings'); 210 | $selected = $options['post_category'] ?? 0; 211 | 212 | $categories = get_categories(['hide_empty' => false]); 213 | 214 | echo ''; 228 | echo 'Select the default category for automatically generated posts.
'; 229 | } 230 | 231 | 232 | public function render_prompt_settings_section(): void {} 233 | 234 | //Render field for AI content generation prompt 235 | public function render_ai_content_prompt_field(): void 236 | { 237 | $options = get_option('aisprtsw_post_settings'); 238 | $value = $options['ai_content_prompt'] ?? ''; 239 | 240 | echo ''; 241 | echo 'Customize the prompt sent to OpenAI for content generation.
'; 242 | } 243 | 244 | 245 | // Render callback for image settings 246 | public function render_image_settings_section(): void 247 | { 248 | echo 'Set the configuration for featured images in posts.
'; 249 | } 250 | // Render field for uploading featured image 251 | public function render_featured_image_upload_field(): void 252 | { 253 | $options = get_option('aisprtsw_post_settings'); 254 | $value = $options['featured_image_url'] ?? ''; 255 | 256 | echo ''; 257 | echo ''; 258 | 259 | $attachment_id = attachment_url_to_postid($value); 260 | 261 | if ($attachment_id) { 262 | // Display the image using wp_get_attachment_image 263 | echo 'When checked, the plugin will attempt to generate a featured image using DALL-E for each post.
'; 280 | } 281 | 282 | 283 | // Sanitize input settings before saving 284 | public function sanitize_settings($input): array 285 | { 286 | $sanitized = []; 287 | 288 | // Max games per day validation 289 | if ($input['max_games_per_day'] < 1 || $input['max_games_per_day'] > 100) { 290 | add_settings_error( 291 | 'aisprtsw_post_settings', 292 | 'invalid_max_games_per_day', 293 | __('Maximum games per day should be between 1 and 100.', 'ai-sports-writer') 294 | ); 295 | $sanitized['max_games_per_day'] = 5; 296 | } else { 297 | $sanitized['max_games_per_day'] = (int) $input['max_games_per_day']; 298 | } 299 | 300 | 301 | // Max games per hour validation 302 | if ($input['max_games_per_hour'] < 1 || $input['max_games_per_hour'] > 24) { 303 | add_settings_error( 304 | 'aisprtsw_post_settings', 305 | 'invalid_max_games_per_hour', 306 | __('Maximum games per hour should be between 1 and 24.', 'ai-sports-writer') 307 | ); 308 | $sanitized['max_games_per_hour'] = 5; 309 | } else { 310 | $sanitized['max_games_per_hour'] = (int) $input['max_games_per_hour']; 311 | } 312 | 313 | // Post intervals validation 314 | $post_intervals = (int) ($input['post_intervals'] ?? 5); 315 | if ($post_intervals < 1 || $post_intervals > 30) { 316 | add_settings_error( 317 | 'aisprtsw_post_settings', 318 | 'invalid_post_intervals', 319 | __('Post intervals should be between 1 and 30 minutes.', 'ai-sports-writer') 320 | ); 321 | $sanitized['post_intervals'] = 5; 322 | } else { 323 | $sanitized['post_intervals'] = $post_intervals; 324 | } 325 | 326 | // Featured image url validator 327 | $featured_image_url = esc_url_raw($input['featured_image_url'] ?? ''); 328 | if (!empty($featured_image_url)) { 329 | $file_type = wp_check_filetype($featured_image_url); 330 | $allowed_image_types = ['jpg', 'jpeg', 'png', 'gif']; 331 | 332 | if (!filter_var($featured_image_url, FILTER_VALIDATE_URL)) { 333 | add_settings_error( 334 | 'aisprtsw_post_settings', 335 | 'invalid_featured_image_url', 336 | __('The featured image URL is not a valid URL.', 'ai-sports-writer') 337 | ); 338 | $sanitized['featured_image_url'] = ''; 339 | } elseif (!in_array($file_type['ext'], $allowed_image_types)) { 340 | add_settings_error( 341 | 'aisprtsw_post_settings', 342 | 'invalid_featured_image_type', 343 | __('The featured image URL does not point to a valid image file (jpg, jpeg, png, or gif).', 'ai-sports-writer') 344 | ); 345 | $sanitized['featured_image_url'] = ''; 346 | } else { 347 | $sanitized['featured_image_url'] = $featured_image_url; 348 | } 349 | } else { 350 | $sanitized['featured_image_url'] = ''; 351 | } 352 | 353 | $sanitized['dalle_image_generation'] = (int) ($input['dalle_image_generation'] ?? 0); 354 | $sanitized['post_author'] = (int) ($input['post_author'] ?? get_current_user_id()); 355 | $sanitized['post_category'] = (int) ($input['post_category'] ?? 0); 356 | 357 | $allowed_html = [ 358 | 'p' => [], 359 | 'br' => [], 360 | 'strong' => [], 361 | 'em' => [], 362 | 'ul' => [], 363 | 'ol' => [], 364 | 'li' => [], 365 | ]; 366 | 367 | $sanitized['ai_content_prompt'] = wp_kses($input['ai_content_prompt'] ?? '', $allowed_html); 368 | $allowed_models = [ 369 | 'gpt-3.5-turbo', 370 | 'gpt-3.5-turbo-16k', 371 | 'gpt-4', 372 | 'gpt-4-turbo', 373 | 'gpt-4o' 374 | ]; 375 | $sanitized['openai_model'] = in_array($input['openai_model'] ?? '', $allowed_models, true) 376 | ? $input['openai_model'] 377 | : 'gpt-3.5-turbo'; 378 | 379 | 380 | return $sanitized; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /includes/Autoloader.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 | -------------------------------------------------------------------------------- /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 |