├── .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 . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Sports Writer 2 | 3 | An AI-powered WordPress plugin that generates engaging match articles from sports data, with customizable content scheduling for bloggers. 4 | 5 | ![Generated Content](screenshots/screenshot3.png) 6 | 7 | ## Description 8 | 9 | AI Sports Writer is a powerful WordPress plugin designed to automate the creation of sports-related content. By harnessing the power of AI and upcoming sports events data, this plugin generates engaging match articles, saving time for sports bloggers and content creators. 10 | 11 | ### Key Features 12 | 13 | - **Automated Content Generation**: Utilizes AI to create unique and engaging sports articles 14 | - **Upcoming Sports Events Data Integration**: Pulls data for upcoming sports events to ensure timely and accurate content generation 15 | - **Customizable Content Scheduling**: Set your preferred publishing times for a consistent content flow 16 | - **User-friendly Interface**: Easy-to-use dashboard for managing all aspects of content generation 17 | - **Multiple Sports Coverage**: Supports various sports for diverse content creation 18 | 19 | ## Installation 20 | 21 | 1. Upload the plugin files to the `/wp-content/plugins/ai-sports-writer` directory, or install the plugin through the WordPress plugins screen directly 22 | 2. Activate the plugin through the 'Plugins' screen in WordPress 23 | 3. Use the Settings->AI Sports Writer screen to configure the API keys and options 24 | -------------------------------------------------------------------------------- /ai-sports-writer.php: -------------------------------------------------------------------------------- 1 | { 35 | $.ajax({ 36 | url: fcg_ajax_object.ajax_url, 37 | type: "POST", 38 | data: { 39 | action: "fetch_regions", 40 | nonce: fcg_ajax_object.nonce, 41 | }, 42 | success: function (response) { 43 | if (response.success) { 44 | const regions = response.data; 45 | const $select = $("#region-selection"); 46 | $select.empty(); // Clear existing options 47 | 48 | regions.forEach((region) => { 49 | let isSelected = ""; 50 | if (region.selected == 1) { 51 | console.log(region.selected); 52 | isSelected = "selected"; 53 | } 54 | $select.append( 55 | `` 56 | ); 57 | }); 58 | } else { 59 | alert("Failed to fetch regions: " + response.data.message); 60 | } 61 | }, 62 | error: function () { 63 | alert("An error occurred while fetching regions."); 64 | }, 65 | }); 66 | }; 67 | 68 | const saveContentRegions = () => { 69 | const selectedRegions = $("#region-selection").val(); 70 | $.ajax({ 71 | url: fcg_ajax_object.ajax_url, 72 | type: "POST", 73 | data: { 74 | action: "save_content_regions", 75 | nonce: fcg_ajax_object.nonce, 76 | selected_regions: selectedRegions, 77 | }, 78 | success: function (response) { 79 | if (response.success) { 80 | alert("Regions saved successfully!"); 81 | } else { 82 | alert("Failed to save regions: " + response.data.message); 83 | } 84 | }, 85 | error: function () { 86 | alert("An error occurred while saving regions."); 87 | }, 88 | }); 89 | }; 90 | 91 | $("#save-regions").click(function (e) { 92 | e.preventDefault(); 93 | saveContentRegions(); 94 | }); 95 | 96 | // Image Upload 97 | $("#upload_featured_image").on("click", function (e) { 98 | e.preventDefault(); 99 | 100 | var image_frame; 101 | if (image_frame) { 102 | image_frame.open(); 103 | } 104 | 105 | image_frame = wp.media({ 106 | title: "Select Featured Image", 107 | multiple: false, 108 | library: { 109 | type: "image", 110 | }, 111 | button: { 112 | text: "Use Image", 113 | }, 114 | }); 115 | 116 | image_frame.on("select", function () { 117 | var attachment = image_frame.state().get("selection").first().toJSON(); 118 | $("#featured_image_url").val(attachment.url); 119 | }); 120 | 121 | image_frame.open(); 122 | }); 123 | 124 | fetchRegions(); 125 | }); 126 | -------------------------------------------------------------------------------- /includes/Admin/ApiConfigPage.php: -------------------------------------------------------------------------------- 1 | 'GPT-3.5 Turbo', 16 | 'gpt-3.5-turbo-16k' => 'GPT-3.5 Turbo 16K', 17 | 'gpt-4' => 'GPT-4', 18 | 'gpt-4-turbo' => 'GPT-4 Turbo', 19 | 'gpt-4o' => 'GPT-4o' 20 | ]; 21 | 22 | /** 23 | * Register hooks and actions 24 | */ 25 | public function register(): void 26 | { 27 | add_action('admin_init', [$this, 'register_settings']); 28 | add_action('wp_ajax_fetch_regions', [$this, 'fetch_regions_ajax']); 29 | add_action('wp_ajax_save_content_regions', [$this, 'save_content_regions_ajax']); 30 | add_action('wp_ajax_test_sport_api', [$this, 'test_sport_api']); 31 | } 32 | 33 | /** 34 | * Register plugin settings 35 | */ 36 | public function register_settings(): void 37 | { 38 | register_setting( 39 | 'aisprtsw_api_settings', 40 | 'aisprtsw_api_settings', 41 | [ 42 | 'sanitize_callback' => [$this, 'sanitize_settings'], 43 | 'default' => [ 44 | 'sport_api_key' => '', 45 | 'openai_api_key' => '', 46 | 'openai_model' => 'gpt-3.5-turbo', 47 | ], 48 | ], 49 | 50 | ); 51 | 52 | // API Configuration Section 53 | add_settings_section( 54 | 'api_settings', 55 | __('API Configuration', 'ai-sports-writer'), 56 | [$this, 'api_settings_section_callback'], 57 | 'ai-sports-writer' 58 | ); 59 | 60 | // Register settings fields 61 | $this->register_api_fields(); 62 | } 63 | 64 | /** 65 | * Register individual API settings fields 66 | */ 67 | private function register_api_fields(): void 68 | { 69 | $fields = [ 70 | 'sport_api_key' => __('Sport API Key', 'ai-sports-writer'), 71 | 'openai_api_key' => __('OpenAI API Key', 'ai-sports-writer'), 72 | 'openai_model' => __('OpenAI Model', 'ai-sports-writer'), 73 | ]; 74 | 75 | foreach ($fields as $field_id => $field_label) { 76 | add_settings_field( 77 | $field_id, 78 | $field_label, 79 | [$this, $field_id . '_callback'], 80 | 'ai-sports-writer', 81 | 'api_settings' 82 | ); 83 | } 84 | } 85 | 86 | /** 87 | * API settings section callback 88 | */ 89 | public function api_settings_section_callback(): void 90 | { 91 | echo '

' . 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 |
30 |

31 |

32 |

'; 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 |
50 | ['interval' => 60 * 60, 'offset' => 10], 62 | 'aisprtsw_fetch_cron' => ['interval' => 3 * 60 * 60, 'offset' => 0], 63 | ]; 64 | } 65 | 66 | /** 67 | * Checks the status of a specific cron hook. 68 | * 69 | * @param string $hookName The name of the cron hook. 70 | * @param int $expectedInterval The expected execution interval in seconds. 71 | * @param int $offsetMinutes The offset in minutes for the cron execution. 72 | * @return string A message indicating the next scheduled run or lack of schedule. 73 | */ 74 | private function checkCronStatus(string $hookName, int $expectedInterval, int $offsetMinutes): string 75 | { 76 | $nextRun = wp_next_scheduled($hookName); 77 | if ($nextRun) { 78 | return sprintf( 79 | // translators: %1$s is the cron hook name, %2$s is the next scheduled time 80 | __('Next run for %1$s is scheduled at %2$s.', 'ai-sports-writer'), 81 | $hookName, 82 | gmdate('Y-m-d H:i:s', $nextRun) 83 | ); 84 | } else { 85 | return sprintf( 86 | // translators: %1$s is the cron hook name 87 | __('No scheduled run found for %1$s.', 'ai-sports-writer'), 88 | $hookName 89 | ); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /includes/Admin/PostConfigPage.php: -------------------------------------------------------------------------------- 1 | [$this, 'sanitize_settings'], 29 | 'default' => [ 30 | 'max_games_per_day' => 5, 31 | 'max_games_per_hour' => 5, 32 | 'post_intervals' => 5, 33 | 'post_author' => get_current_user_id(), 34 | 'post_category' => 0, 35 | 'ai_content_prompt' => "You are a passionate football blogger writing for fans who love deep match insights. Your goal is to create an engaging, narrative-driven preview that feels like a conversation with a knowledgeable friend at a sports bar. 36 | 37 | Writing Instructions: 38 | 1. Write in a conversational, passionate tone as if discussing the match with a close friend 39 | 2. Provide context beyond raw statistics - discuss team dynamics and potential match narratives 40 | 3. Include a balanced, nuanced prediction that considers both statistical likelihood and the unpredictable nature of football 41 | 4. Use engaging storytelling techniques to make the preview compelling 42 | 5. Incorporate the betting odds context subtly, focusing on match analysis rather than pure gambling perspective 43 | 6. Aim for 500-700 words 44 | 7. End with a provocative question or intriguing prediction to spark reader engagement 45 | 46 | Special Requests: 47 | - Avoid generic sports clichés 48 | - Use vivid, descriptive language 49 | - Highlight potential match-defining moments 50 | - Create a sense of anticipation and excitement", 51 | 'featured_image_url' => '', 52 | 'dalle_image_generation' => 0, 53 | 'openai_model' => 'gpt-3.5-turbo', 54 | ], 55 | ] 56 | ); 57 | 58 | // Post Configuration Section 59 | add_settings_section( 60 | 'post_settings', 61 | '', 62 | [$this, 'render_post_settings_section'], 63 | 'ai-sports-writer-post' 64 | ); 65 | 66 | add_settings_field( 67 | 'max_games_per_day', 68 | 'Maximum Games Per Day', 69 | [$this, 'render_max_games_per_day_field'], 70 | 'ai-sports-writer-post', 71 | 'post_settings' 72 | ); 73 | 74 | add_settings_field( 75 | 'max_games_per_hour', 76 | 'Maximum Games Per Hour', 77 | [$this, 'render_max_games_per_hour_field'], 78 | 'ai-sports-writer-post', 79 | 'post_settings' 80 | ); 81 | 82 | add_settings_field( 83 | 'post_intervals', 84 | 'Post Intervals (minutes)', 85 | [$this, 'render_post_intervals_field'], 86 | 'ai-sports-writer-post', 87 | 'post_settings' 88 | ); 89 | 90 | add_settings_field( 91 | 'post_author', 92 | 'Default Post Author', 93 | [$this, 'render_post_author_field'], 94 | 'ai-sports-writer-post', 95 | 'post_settings' 96 | ); 97 | 98 | add_settings_field( 99 | 'post_category', 100 | 'Default Post Category', 101 | [$this, 'render_post_category_field'], 102 | 'ai-sports-writer-post', 103 | 'post_settings' 104 | ); 105 | 106 | 107 | // Prompt Configuration Section 108 | add_settings_section( 109 | 'prompt_settings', 110 | 'AI Prompt Configuration', 111 | [$this, 'render_prompt_settings_section'], 112 | 'ai-sports-writer-post' 113 | ); 114 | 115 | add_settings_field( 116 | 'ai_content_prompt', 117 | 'AI Content Generation Prompt', 118 | [$this, 'render_ai_content_prompt_field'], 119 | 'ai-sports-writer-post', 120 | 'prompt_settings' 121 | ); 122 | 123 | 124 | 125 | // Featured Image Section 126 | add_settings_section( 127 | 'image_settings', 128 | 'Featured Image Settings', 129 | [$this, 'render_image_settings_section'], 130 | 'ai-sports-writer-post' 131 | ); 132 | 133 | add_settings_field( 134 | 'featured_image_upload', 135 | 'Featured Image', 136 | [$this, 'render_featured_image_upload_field'], 137 | 'ai-sports-writer-post', 138 | 'image_settings' 139 | ); 140 | 141 | add_settings_field( 142 | 'dalle_image_generation', 143 | 'DALL-E Image Generation', 144 | [$this, 'render_dalle_image_generation_field'], 145 | 'ai-sports-writer-post', 146 | 'image_settings' 147 | ); 148 | } 149 | 150 | 151 | // Render callback for post settings 152 | public function render_post_settings_section(): void 153 | { 154 | echo '

' . 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 '
'; 264 | echo wp_get_attachment_image($attachment_id, 'medium'); 265 | echo '
'; 266 | } 267 | } 268 | 269 | // Render checkbox for DALL-E image generation 270 | public function render_dalle_image_generation_field(): void 271 | { 272 | $options = get_option('aisprtsw_post_settings'); 273 | $checked = checked(1, ($options['dalle_image_generation'] ?? 0), false); 274 | 275 | echo ''; 279 | 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 |
248 |

249 | 250 |
251 | 257 |
258 | 259 |
260 | 261 | 270 |
271 |

272 |
273 | 278 |
279 |
280 | renderPage(); 290 | ?> 291 |
292 | 293 |
294 | 299 |
300 |
301 | get_charset_collate(); 316 | $table_prefix = $wpdb->prefix; 317 | 318 | $tables = [ 319 | 'content_regions' => "CREATE TABLE IF NOT EXISTS {$table_prefix}content_regions ( 320 | id INT AUTO_INCREMENT PRIMARY KEY, 321 | region_id INT NOT NULL UNIQUE 322 | ) $charset_collate;", 323 | 324 | 'football_regions' => "CREATE TABLE IF NOT EXISTS {$table_prefix}football_regions ( 325 | id INT AUTO_INCREMENT PRIMARY KEY, 326 | name VARCHAR(255) UNIQUE NOT NULL, 327 | leagues TEXT NOT NULL 328 | ) $charset_collate;", 329 | 330 | 'football_games' => "CREATE TABLE IF NOT EXISTS {$table_prefix}football_games ( 331 | id BIGINT NOT NULL AUTO_INCREMENT, 332 | match_code VARCHAR(255), 333 | region VARCHAR(255), 334 | team VARCHAR(255), 335 | home VARCHAR(255), 336 | away VARCHAR(255), 337 | match_datetime DATETIME, 338 | time_zone VARCHAR(50), 339 | provider VARCHAR(100), 340 | odds TEXT, 341 | processed BOOLEAN DEFAULT 0, 342 | processed_started_at TIMESTAMP NULL, 343 | process_completed_at TIMESTAMP NULL, 344 | processed_failed_at TIMESTAMP NULL, 345 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 346 | PRIMARY KEY (id), 347 | UNIQUE KEY match_code (match_code) 348 | ) $charset_collate;" 349 | ]; 350 | 351 | foreach ($tables as $table_name => $sql) { 352 | $result = dbDelta($sql); 353 | 354 | if ($result === false) { 355 | throw new Exception(sprintf( 356 | /* translators: %s: table name */ 357 | esc_html__('Failed to create table: %s', 'ai-sports-writer'), 358 | esc_html($table_name) 359 | )); 360 | } 361 | } 362 | 363 | update_option('aisprtsw_db_version', self::PLUGIN_VERSION); 364 | } 365 | 366 | /** 367 | * Logging method 368 | * 369 | * @param string $message Log message 370 | * @param string $level Log level 371 | */ 372 | private function log(string $message, string $level = 'info'): void 373 | { 374 | if (!defined('WP_DEBUG') || !WP_DEBUG) { 375 | return; 376 | } 377 | 378 | $log_message = sprintf( 379 | '[%s] [%s] %s', 380 | self::PLUGIN_DOMAIN, 381 | strtoupper($level), 382 | $message 383 | ); 384 | } 385 | 386 | /** 387 | * Prevent cloning of the instance 388 | */ 389 | private function __clone() {} 390 | 391 | /** 392 | * Prevent unserializing of the instance 393 | */ 394 | public function __wakeup() {} 395 | } 396 | -------------------------------------------------------------------------------- /includes/Services/OpenAiService.php: -------------------------------------------------------------------------------- 1 | 'POST', 32 | 'timeout' => 30, 33 | 'headers' => [ 34 | 'Authorization' => 'Bearer ' . $apiKey, 35 | 'Content-Type' => 'application/json', 36 | ], 37 | 'body' => wp_json_encode([ 38 | 'model' => $model, 39 | 'messages' => [ 40 | [ 41 | 'role' => 'system', 42 | 'content' => 'You are a professional sports content writer.' 43 | ], 44 | [ 45 | 'role' => 'user', 46 | 'content' => $prompt 47 | ] 48 | ] 49 | ]) 50 | ]; 51 | 52 | $response = wp_remote_post(self::API_ENDPOINT, $args); 53 | 54 | if (is_wp_error($response)) { 55 | Logger::log('OpenAI API request failed: ' . $response->get_error_message(), 'error'); 56 | return null; 57 | } 58 | 59 | $httpCode = wp_remote_retrieve_response_code($response); 60 | if ($httpCode !== 200) { 61 | Logger::log("OpenAI API request failed with HTTP code {$httpCode}.", 'error'); 62 | return null; 63 | } 64 | 65 | $responseBody = wp_remote_retrieve_body($response); 66 | $responseData = json_decode($responseBody, true); 67 | 68 | if (json_last_error() !== JSON_ERROR_NONE) { 69 | Logger::log('Failed to decode JSON response from OpenAI API.', 'error'); 70 | return null; 71 | } 72 | 73 | return $responseData['choices'][0]['message']['content'] ?? null; 74 | } 75 | 76 | 77 | 78 | /** 79 | * Generates a title for a blog post using the OpenAI API. 80 | * 81 | * @param string $content The blog post content. 82 | * @param string $apiKey The OpenAI API key. 83 | * @return string The generated title or an empty string on failure. 84 | */ 85 | public function generateTitle(string $content, string $apiKey): string 86 | { 87 | if (empty($apiKey)) { 88 | Logger::log('OpenAI API key is missing for title generation.', 'error'); 89 | return ''; 90 | } 91 | 92 | $args = [ 93 | 'method' => 'POST', 94 | 'timeout' => 30, 95 | 'headers' => [ 96 | 'Authorization' => 'Bearer ' . $apiKey, 97 | 'Content-Type' => 'application/json', 98 | ], 99 | 'body' => wp_json_encode([ 100 | 'model' => 'gpt-3.5-turbo', 101 | 'messages' => [ 102 | [ 103 | 'role' => 'system', 104 | 'content' => 'You are a title generator. Create a concise, engaging title for a blog post based on the given content.' 105 | ], 106 | [ 107 | 'role' => 'user', 108 | 'content' => "Generate a compelling title for this blog post content:\n\n" . $content 109 | ] 110 | ] 111 | ]) 112 | ]; 113 | 114 | $response = wp_remote_post(self::API_ENDPOINT, $args); 115 | 116 | 117 | if (is_wp_error($response)) { 118 | Logger::log('OpenAI title generation request failed: ' . $response->get_error_message(), 'error'); 119 | return ''; 120 | } 121 | 122 | $httpCode = wp_remote_retrieve_response_code($response); 123 | if ($httpCode !== 200) { 124 | Logger::log('OpenAI title generation failed with HTTP code ' . $httpCode, 'error'); 125 | return ''; 126 | } 127 | 128 | 129 | $responseBody = wp_remote_retrieve_body($response); 130 | $responseData = json_decode($responseBody, true); 131 | 132 | if (json_last_error() !== JSON_ERROR_NONE) { 133 | Logger::log('Failed to parse JSON response from OpenAI for title generation.', 'error'); 134 | return ''; 135 | } 136 | 137 | 138 | $generatedTitle = $responseData['choices'][0]['message']['content'] ?? ''; 139 | return trim($generatedTitle, '"'); // Remove any extra quotes 140 | } 141 | 142 | 143 | 144 | /** 145 | * Generate an image using DALL-E API. 146 | * 147 | * @param string $openai_api_key OpenAI API key. 148 | * @param array $game Game data (team names). 149 | * @return string|null URL of the generated image or null on failure. 150 | */ 151 | public function generateDalleImage($openai_api_key, $game) 152 | { 153 | // Validate API key 154 | if (empty($openai_api_key)) { 155 | Logger::log('OpenAI API key is missing for DALL-E image generation', 'ERROR'); 156 | return null; 157 | } 158 | 159 | // Create image prompt 160 | $imagePrompt = isset($game['home'], $game['away']) 161 | ? sprintf( 162 | 'A dynamic football stadium scene with %s and %s jerseys, vibrant sports photography style', 163 | addslashes(sanitize_text_field($game['home'])), 164 | addslashes(sanitize_text_field($game['away'])) 165 | ) 166 | : 'A football match preview poster with stadium and players'; 167 | 168 | $url = 'https://api.openai.com/v1/images/generations'; 169 | 170 | $args = [ 171 | 'method' => 'POST', 172 | 'timeout' => 30, 173 | 'headers' => [ 174 | 'Authorization' => 'Bearer ' . $openai_api_key, 175 | 'Content-Type' => 'application/json', 176 | ], 177 | 'body' => wp_json_encode([ 178 | 'model' => 'dall-e-3', 179 | 'prompt' => $imagePrompt, 180 | 'n' => 1, 181 | 'size' => '1024x1024' 182 | ]) 183 | ]; 184 | 185 | $response = wp_remote_post($url, $args); 186 | 187 | // Detailed error handling 188 | if (is_wp_error($response)) { 189 | $error_message = $response->get_error_message(); 190 | Logger::log('DALL-E Request Error: ' . $error_message, 'ERROR'); 191 | return null; 192 | } 193 | 194 | // Check HTTP response code 195 | $http_code = wp_remote_retrieve_response_code($response); 196 | if ($http_code !== 200) { 197 | $response_body = wp_remote_retrieve_body($response); 198 | Logger::log('DALL-E HTTP Error Code: ' . $http_code, 'ERROR'); 199 | Logger::log('DALL-E Response Body: ' . $response_body); 200 | return null; 201 | } 202 | 203 | // Parse response 204 | $response_body = wp_remote_retrieve_body($response); 205 | $response_data = json_decode($response_body, true); 206 | 207 | // Validate response data 208 | if (json_last_error() !== JSON_ERROR_NONE) { 209 | Logger::log('JSON Decode Error: ' . json_last_error_msg(), 'ERROR'); 210 | Logger::log('Raw Response: ' . $response_body); 211 | return null; 212 | } 213 | 214 | // Check for image URL 215 | if (isset($response_data['data'][0]['url'])) { 216 | return $response_data['data'][0]['url']; 217 | } 218 | 219 | // Log if no image URL found 220 | Logger::log('No image URL found in DALL-E response'); 221 | return null; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /includes/Services/SportApiService.php: -------------------------------------------------------------------------------- 1 | makeApiRequest('/regions', $apiKey); 20 | return $regions !== null ? $regions : []; 21 | } 22 | 23 | 24 | /** 25 | * Fetches game statistics for a specific match. 26 | * 27 | * @param string $apiKey The API key used for authorization. 28 | * @param string $matchCode The unique identifier for the match. 29 | * @return array|null An associative array containing the stats, or null if an error occurs. 30 | */ 31 | public function fetchGameStatistics(string $apiKey, string $matchCode): ?array 32 | { 33 | $endpoints = [ 34 | 'home_matches' => "/stats/{$matchCode}/home-matches", 35 | 'away_matches' => "/stats/{$matchCode}/away-matches", 36 | 'head_to_head' => "/stats/{$matchCode}/head-to-head", 37 | ]; 38 | 39 | $allStats = []; 40 | foreach ($endpoints as $key => $endpoint) { 41 | $response = $this->makeApiRequest($endpoint, $apiKey); 42 | if ($response === null || isset($response['error'])) { 43 | return null; 44 | } 45 | $allStats[$key] = $response['data'] ?? []; 46 | } 47 | 48 | return $allStats; 49 | } 50 | 51 | /** 52 | * Makes a request to the API and returns the response data. 53 | * 54 | * @param string $endpoint The API endpoint to fetch data from. 55 | * @param string $apiKey The API key used for authorization. 56 | * @return array|null The decoded JSON response from the API, or null if an error occurs. 57 | */ 58 | private function makeApiRequest(string $endpoint, string $apiKey): ?array 59 | { 60 | $url = self::API_BASE_URL . $endpoint; 61 | 62 | $args = [ 63 | 'method' => 'GET', 64 | 'timeout' => 30, 65 | 'headers' => [ 66 | 'Authorization' => 'Bearer ' . $apiKey, 67 | 'Content-Type' => 'application/json', 68 | ], 69 | ]; 70 | 71 | $response = wp_remote_get($url, $args); 72 | 73 | if (is_wp_error($response)) { 74 | Logger::log("API request failed for $endpoint: " . $response->get_error_message(), 'error'); 75 | return null; 76 | } 77 | 78 | $httpCode = wp_remote_retrieve_response_code($response); 79 | if ($httpCode !== 200) { 80 | Logger::log("API request failed for $endpoint with HTTP code $httpCode", 'error'); 81 | return null; 82 | } 83 | 84 | $responseBody = wp_remote_retrieve_body($response); 85 | $decodedResponse = json_decode($responseBody, true); 86 | 87 | if (json_last_error() !== JSON_ERROR_NONE) { 88 | Logger::log("Failed to decode JSON response for $endpoint", 'error'); 89 | return null; 90 | } 91 | 92 | return $decodedResponse; 93 | } 94 | 95 | /** 96 | * Fetches the upcoming games from the API. 97 | * 98 | * @param string $apiKey The API key used for authorization. 99 | * @return array|null The array of upcoming games fetched from the API, or null if an error occurs. 100 | */ 101 | public function fetchUpcomingEndpoint(string $apiKey): ?array 102 | { 103 | return $this->makeApiRequest('/games', $apiKey); 104 | } 105 | } -------------------------------------------------------------------------------- /includes/Utilities/Logger.php: -------------------------------------------------------------------------------- 1 | '; 31 | } 32 | } 33 | 34 | private static function is_ajax_or_cli_request(): bool 35 | { 36 | return (function_exists('wp_doing_ajax') && wp_doing_ajax()) || php_sapi_name() === 'cli'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWithKola/AI-Sports-Writer/06362bceb42ab1c2fc8946651932c970e9859705/screenshots/screenshot3.png --------------------------------------------------------------------------------