├── screenshots └── screenshot3.png ├── LICENSE ├── ai-sports-writer.php ├── .gitignore ├── includes ├── Utilities │ └── Logger.php ├── Autoloader.php ├── Admin │ ├── CronSettingsPage.php │ ├── ApiConfigPage.php │ └── PostConfigPage.php ├── Services │ ├── SportApiService.php │ └── OpenAiService.php └── Core │ ├── Plugin.php │ └── ContentGenerator.php ├── README.md └── assets └── js └── ai-sports-writer.js /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWithKola/AI-Sports-Writer/HEAD/screenshots/screenshot3.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | AI Sports Writer Plugin 2 | Copyright (C) 2024 Kolawole Yusuf 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /ai-sports-writer.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 | -------------------------------------------------------------------------------- /includes/Autoloader.php: -------------------------------------------------------------------------------- 1 | AI Sports Writer screen to configure the API keys and options 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/js/ai-sports-writer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initializes the AI Sports Writer plugin functionality when the DOM is ready. 3 | * This function sets up event listeners for API testing, region fetching and saving, 4 | * and image uploading. 5 | * 6 | * @param {function($)} $ - The jQuery function 7 | * @returns {void} 8 | */ 9 | jQuery(document).ready(function ($) { 10 | $("#test-sport-api").on("click", function (e) { 11 | e.preventDefault(); 12 | 13 | const button = $(this); 14 | button.prop("disabled", true).text("Testing..."); 15 | 16 | $.post( 17 | fcg_ajax_object.ajax_url, 18 | { 19 | action: "test_sport_api", 20 | _ajax_nonce: fcg_ajax_object.nonce, 21 | }, 22 | function (response) { 23 | if (response.success) { 24 | button.text("OK"); 25 | fetchRegions(); 26 | } else { 27 | button.text("Test API").prop("disabled", false); 28 | alert(response.data.message || "An error occurred"); 29 | } 30 | } 31 | ); 32 | }); 33 | 34 | const fetchRegions = () => { 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/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/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 | // Get user settings for image generation 160 | $post_options = get_option('aisprtsw_post_settings'); 161 | $image_size = $post_options['dalle_image_size'] ?? '1024x1024'; 162 | $image_quality = $post_options['dalle_image_quality'] ?? 'standard'; 163 | 164 | // Create image prompt based on size for better composition 165 | $imagePrompt = isset($game['home'], $game['away']) 166 | ? $this->createContextualPrompt($game, $image_size) 167 | : $this->createFallbackPrompt($image_size); 168 | 169 | $url = 'https://api.openai.com/v1/images/generations'; 170 | 171 | // Build request body 172 | $request_body = [ 173 | 'model' => 'dall-e-3', 174 | 'prompt' => $imagePrompt, 175 | 'n' => 1, 176 | 'size' => $image_size 177 | ]; 178 | 179 | // Add quality parameter if HD is selected 180 | if ($image_quality === 'hd') { 181 | $request_body['quality'] = 'hd'; 182 | } 183 | 184 | $args = [ 185 | 'method' => 'POST', 186 | 'timeout' => 30, 187 | 'headers' => [ 188 | 'Authorization' => 'Bearer ' . $openai_api_key, 189 | 'Content-Type' => 'application/json', 190 | ], 191 | 'body' => wp_json_encode($request_body) 192 | ]; 193 | 194 | $response = wp_remote_post($url, $args); 195 | 196 | // Detailed error handling 197 | if (is_wp_error($response)) { 198 | $error_message = $response->get_error_message(); 199 | Logger::log('DALL-E Request Error: ' . $error_message, 'ERROR'); 200 | return null; 201 | } 202 | 203 | // Check HTTP response code 204 | $http_code = wp_remote_retrieve_response_code($response); 205 | if ($http_code !== 200) { 206 | $response_body = wp_remote_retrieve_body($response); 207 | Logger::log('DALL-E HTTP Error Code: ' . $http_code, 'ERROR'); 208 | Logger::log('DALL-E Response Body: ' . $response_body); 209 | return null; 210 | } 211 | 212 | // Parse response 213 | $response_body = wp_remote_retrieve_body($response); 214 | $response_data = json_decode($response_body, true); 215 | 216 | // Validate response data 217 | if (json_last_error() !== JSON_ERROR_NONE) { 218 | Logger::log('JSON Decode Error: ' . json_last_error_msg(), 'ERROR'); 219 | Logger::log('Raw Response: ' . $response_body); 220 | return null; 221 | } 222 | 223 | // Check for image URL 224 | if (isset($response_data['data'][0]['url'])) { 225 | return $response_data['data'][0]['url']; 226 | } 227 | 228 | // Log if no image URL found 229 | Logger::log('No image URL found in DALL-E response'); 230 | return null; 231 | } 232 | 233 | /** 234 | * Create contextual prompt based on game data and image size 235 | * 236 | * @param array $game Game data 237 | * @param string $size Image size 238 | * @return string Optimized prompt 239 | */ 240 | private function createContextualPrompt($game, $size) 241 | { 242 | $home_team = addslashes(sanitize_text_field($game['home'])); 243 | $away_team = addslashes(sanitize_text_field($game['away'])); 244 | 245 | switch ($size) { 246 | case '1792x1024': // Landscape - Stadium scenes 247 | return sprintf( 248 | 'Wide panoramic view of a football stadium during %s vs %s match, dynamic crowd atmosphere, team colors prominently displayed, professional sports photography style, vibrant lighting', 249 | $home_team, 250 | $away_team 251 | ); 252 | 253 | case '1024x1792': // Portrait - Social media 254 | return sprintf( 255 | 'Vertical composition football match poster for %s vs %s, bold team logos, dynamic player silhouettes, modern graphic design, social media optimized layout', 256 | $home_team, 257 | $away_team 258 | ); 259 | 260 | case '1024x1024': // Square - Classic format 261 | default: 262 | return sprintf( 263 | 'Dynamic football stadium scene with %s and %s jerseys, vibrant sports photography style, balanced composition', 264 | $home_team, 265 | $away_team 266 | ); 267 | } 268 | } 269 | 270 | /** 271 | * Create fallback prompt when game data is not available 272 | * 273 | * @param string $size Image size 274 | * @return string Fallback prompt 275 | */ 276 | private function createFallbackPrompt($size) 277 | { 278 | switch ($size) { 279 | case '1792x1024': // Landscape 280 | return 'Wide panoramic football stadium view with dramatic lighting, crowd atmosphere, professional sports photography'; 281 | 282 | case '1024x1792': // Portrait 283 | return 'Vertical football match preview poster with dynamic design, modern sports graphics, social media format'; 284 | 285 | case '1024x1024': // Square 286 | default: 287 | return 'Football match preview poster with stadium and players, balanced composition'; 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /includes/Admin/ApiConfigPage.php: -------------------------------------------------------------------------------- 1 | 'GPT-3.5 Turbo', 16 | 'gpt-4o-mini' => 'GPT-4o Mini', 17 | 'gpt-4-turbo' => 'GPT-4 Turbo', 18 | 'gpt-4o' => 'GPT-4o', 19 | 'gpt-4o-2024-08-06' => 'GPT-4o (Latest)', 20 | 'o1-mini' => 'o1-mini (Reasoning)' 21 | ]; 22 | 23 | /** 24 | * Register hooks and actions 25 | */ 26 | public function register(): void 27 | { 28 | add_action('admin_init', [$this, 'register_settings']); 29 | add_action('wp_ajax_fetch_regions', [$this, 'fetch_regions_ajax']); 30 | add_action('wp_ajax_save_content_regions', [$this, 'save_content_regions_ajax']); 31 | add_action('wp_ajax_test_sport_api', [$this, 'test_sport_api']); 32 | } 33 | 34 | /** 35 | * Register plugin settings 36 | */ 37 | public function register_settings(): void 38 | { 39 | register_setting( 40 | 'aisprtsw_api_settings', 41 | 'aisprtsw_api_settings', 42 | [ 43 | 'sanitize_callback' => [$this, 'sanitize_settings'], 44 | 'default' => [ 45 | 'sport_api_key' => '', 46 | 'openai_api_key' => '', 47 | 'openai_model' => 'gpt-3.5-turbo', 48 | ], 49 | ], 50 | 51 | ); 52 | 53 | // API Configuration Section 54 | add_settings_section( 55 | 'api_settings', 56 | __('API Configuration', 'ai-sports-writer'), 57 | [$this, 'api_settings_section_callback'], 58 | 'ai-sports-writer' 59 | ); 60 | 61 | // Register settings fields 62 | $this->register_api_fields(); 63 | } 64 | 65 | /** 66 | * Register individual API settings fields 67 | */ 68 | private function register_api_fields(): void 69 | { 70 | $fields = [ 71 | 'sport_api_key' => __('Sport API Key', 'ai-sports-writer'), 72 | 'openai_api_key' => __('OpenAI API Key', 'ai-sports-writer'), 73 | 'openai_model' => __('OpenAI Model', 'ai-sports-writer'), 74 | ]; 75 | 76 | foreach ($fields as $field_id => $field_label) { 77 | add_settings_field( 78 | $field_id, 79 | $field_label, 80 | [$this, $field_id . '_callback'], 81 | 'ai-sports-writer', 82 | 'api_settings' 83 | ); 84 | } 85 | } 86 | 87 | /** 88 | * API settings section callback 89 | */ 90 | public function api_settings_section_callback(): void 91 | { 92 | echo '

' . esc_html__('Configure your API settings here. Enter the required keys and test API connectivity.', 'ai-sports-writer') . '

'; 93 | } 94 | 95 | /** 96 | * Sport API key input callback 97 | */ 98 | public function sport_api_key_callback(): void 99 | { 100 | $options = get_option('aisprtsw_api_settings'); 101 | $sport_api_key = $options['sport_api_key'] ?? ''; 102 | 103 | printf( 104 | ' 105 |

%s

', 106 | esc_attr('aisprtsw_api_settings'), 107 | esc_attr($sport_api_key), 108 | esc_html__('Enter your API key from scalesp.com. You can get an API key by signing up or logging into your account on scalesp.com.', 'ai-sports-writer') 109 | ); 110 | } 111 | 112 | /** 113 | * OpenAI API key input callback 114 | */ 115 | public function openai_api_key_callback(): void 116 | { 117 | $options = get_option('aisprtsw_api_settings'); 118 | $openai_api_key = $options['openai_api_key'] ?? ''; 119 | 120 | printf( 121 | ' 122 |

%s

', 123 | esc_attr('aisprtsw_api_settings'), 124 | esc_attr($openai_api_key), 125 | esc_html__('Enter your OpenAI API key. You can obtain an API key by signing up or logging into your account on openai.com.', 'ai-sports-writer') 126 | ); 127 | } 128 | 129 | /** 130 | * OpenAI model selection callback 131 | */ 132 | public function openai_model_callback(): void 133 | { 134 | $options = get_option('aisprtsw_api_settings'); 135 | $current_model = $options['openai_model'] ?? 'gpt-3.5-turbo'; 136 | 137 | // Build options array 138 | $model_options = array_map(function ($model_key, $model_name) use ($current_model) { 139 | return sprintf( 140 | '', 141 | esc_attr($model_key), 142 | selected($current_model, $model_key, false), 143 | esc_html($model_name) 144 | ); 145 | }, array_keys(self::ALLOWED_MODELS), self::ALLOWED_MODELS); 146 | 147 | $model_options = wp_kses( 148 | implode('', $model_options), 149 | [ 150 | 'option' => [ 151 | 'value' => [], 152 | 'selected' => [] 153 | ] 154 | ] 155 | ); 156 | 157 | printf( 158 | ' 159 |

%s

', 160 | esc_attr('aisprtsw_api_settings'), 161 | $model_options, 162 | esc_html__('Select the OpenAI model to use for content generation.', 'ai-sports-writer') 163 | ); 164 | } 165 | 166 | /** 167 | * Test Sport API Ajax handler 168 | */ 169 | public function test_sport_api(): void 170 | { 171 | // Verify nonce for security 172 | check_ajax_referer('fcg_nonce', 'nonce'); 173 | 174 | // Get the API key from options 175 | $options = get_option('aisprtsw_api_settings'); 176 | $api_key = $options['sport_api_key'] ?? ''; 177 | 178 | // Check if the API key is provided 179 | if (empty($api_key)) { 180 | wp_send_json_error(['message' => __('API Key is missing', 'ai-sports-writer')]); 181 | return; 182 | } 183 | 184 | try { 185 | // Initialize the SportApiService 186 | $sport_api_service = new \AiSprtsW\Services\SportApiService(); 187 | 188 | // Fetch regions 189 | $regions = $sport_api_service->fetchFootballRegions($api_key); 190 | 191 | // Insert regions into database 192 | $this->insert_regions_into_db($regions); 193 | 194 | // Send success response 195 | wp_send_json_success(['message' => __('API Connection Successful', 'ai-sports-writer')]); 196 | } catch (\Exception $e) { 197 | // Log the error and send failure response 198 | Logger::log('Sport API Test Error: ' . $e->getMessage(), 'ERROR'); 199 | wp_send_json_error(['message' => sprintf( 200 | // translators: %s is the error message returned from the exception 201 | __('Request failed: %s', 'ai-sports-writer'), 202 | $e->getMessage() 203 | )]); 204 | } 205 | } 206 | 207 | /** 208 | * Insert regions into database 209 | * 210 | * @param array $regions Regions data to insert 211 | */ 212 | private function insert_regions_into_db(array $regions): void 213 | { 214 | global $wpdb; 215 | $regions = $regions['data'] ?? []; 216 | $table_name = esc_sql($wpdb->prefix . 'football_regions'); 217 | 218 | // Prepare and insert regions 219 | foreach ($regions as $region) { 220 | $name = sanitize_text_field($region['name']); 221 | $leagues = wp_json_encode($region['leagues']); 222 | 223 | // Check if region exists 224 | $existing_region = $wpdb->get_var( 225 | $wpdb->prepare( 226 | "SELECT COUNT(*) FROM {$table_name} WHERE name = %s", 227 | $name 228 | ) 229 | ); 230 | 231 | if ($existing_region) { 232 | continue; // Skip duplicate region 233 | } 234 | 235 | // Insert new region 236 | $wpdb->insert( 237 | $table_name, 238 | [ 239 | 'name' => $name, 240 | 'leagues' => $leagues, 241 | ], 242 | ['%s', '%s'] 243 | ); 244 | } 245 | } 246 | 247 | 248 | 249 | /** 250 | * Fetch regions via Ajax 251 | */ 252 | public function fetch_regions_ajax(): void 253 | { 254 | // Verify nonce 255 | check_ajax_referer('fcg_nonce', 'nonce'); 256 | 257 | global $wpdb; 258 | 259 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 260 | $regions_table = esc_sql($wpdb->prefix . 'football_regions'); 261 | 262 | // Fetch regions with selection status 263 | $regions = $wpdb->get_results( 264 | " 265 | SELECT r.*, 266 | (SELECT COUNT(*) FROM {$content_regions_table} cr WHERE cr.region_id = r.id) > 0 as selected 267 | FROM {$regions_table} r 268 | ", 269 | ARRAY_A 270 | ); 271 | 272 | wp_send_json_success($regions); 273 | } 274 | 275 | 276 | 277 | /** 278 | * Save content regions via Ajax 279 | */ 280 | public function save_content_regions_ajax(): void 281 | { 282 | // Verify nonce 283 | check_ajax_referer('fcg_nonce', 'nonce'); 284 | 285 | global $wpdb; 286 | 287 | // Sanitize the table name 288 | $content_regions_table = esc_sql($wpdb->prefix . 'content_regions'); 289 | 290 | // Sanitize and validate selected regions 291 | $selected_regions = isset($_POST['selected_regions']) 292 | ? array_map('intval', (array)$_POST['selected_regions']) 293 | : []; 294 | 295 | if (empty($selected_regions)) { 296 | wp_send_json_error(['message' => __('No regions selected.', 'ai-sports-writer')]); 297 | return; 298 | } 299 | 300 | // Clear existing selections 301 | $wpdb->query("TRUNCATE TABLE {$content_regions_table}"); 302 | 303 | // Insert new selections 304 | foreach ($selected_regions as $region_id) { 305 | $wpdb->insert( 306 | $content_regions_table, 307 | ['region_id' => $region_id], 308 | ['%d'] 309 | ); 310 | } 311 | 312 | wp_send_json_success(['message' => __('Regions saved successfully.', 'ai-sports-writer')]); 313 | } 314 | 315 | 316 | /** 317 | * Sanitize and validate settings 318 | * 319 | * @param array $input Unsanitized input settings 320 | * @return array Sanitized settings 321 | */ 322 | public function sanitize_settings(array $input): array 323 | { 324 | $sanitized = []; 325 | 326 | // Sanitize API keys 327 | $sanitized['sport_api_key'] = sanitize_text_field($input['sport_api_key'] ?? ''); 328 | $sanitized['openai_api_key'] = sanitize_text_field($input['openai_api_key'] ?? ''); 329 | 330 | // Validate OpenAI model 331 | if (isset($input['openai_model']) && array_key_exists($input['openai_model'], self::ALLOWED_MODELS)) { 332 | $sanitized['openai_model'] = $input['openai_model']; 333 | } else { 334 | $sanitized['openai_model'] = 'gpt-3.5-turbo'; 335 | add_settings_error( 336 | 'aisprtsw_api_settings', 337 | 'invalid_openai_model', 338 | __('Invalid OpenAI model selected. Defaulting to GPT-3.5 Turbo.', 'ai-sports-writer') 339 | ); 340 | } 341 | 342 | return $sanitized; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /includes/Core/Plugin.php: -------------------------------------------------------------------------------- 1 | 0, 52 | 'warning' => 1, 53 | 'error' => 2 54 | ]; 55 | 56 | /** 57 | * Private constructor to prevent direct instantiation 58 | */ 59 | private function __construct() 60 | { 61 | $this->initializePages(); 62 | $this->setupHooks(); 63 | } 64 | 65 | /** 66 | * Singleton instance getter 67 | * 68 | * @return self 69 | */ 70 | public static function init() 71 | { 72 | if (self::$instance === null) { 73 | self::$instance = new self(); 74 | 75 | // Register activation and deactivation hooks 76 | register_activation_hook(AISPRTSW_PLUGIN_FILE, [self::$instance, 'activate']); 77 | register_deactivation_hook(AISPRTSW_PLUGIN_FILE, [self::$instance, 'deactivate']); 78 | add_action('init', [self::class, 'initialize_content_generator']); 79 | } 80 | return self::$instance; 81 | } 82 | 83 | /** 84 | * Plugin activation method 85 | */ 86 | public function activate(): void 87 | { 88 | // Ensure plugin is only activated by administrators 89 | if (!current_user_can('activate_plugins')) { 90 | return; 91 | } 92 | 93 | try { 94 | $this->create_sport_ai_writer_tables(); 95 | $this->log('Plugin activated successfully'); 96 | } catch (Exception $e) { 97 | $this->log("Activation failed: {$e->getMessage()}", 'error'); 98 | 99 | // Prevent plugin activation and show error 100 | 101 | wp_die( 102 | sprintf( 103 | // Translators: %s is the error message from an exception during plugin activation 104 | esc_html__('AI Sports Writer could not be activated. Error: %s', 'ai-sports-writer'), 105 | esc_html($e->getMessage()) 106 | ), 107 | esc_html__('Plugin Activation Error', 'ai-sports-writer'), 108 | ['response' => 500] 109 | ); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Plugin deactivation method 116 | */ 117 | public function deactivate(): void 118 | { 119 | // Restrict plugin deactivation to administrators only. 120 | if (!current_user_can('activate_plugins')) { 121 | return; 122 | } 123 | 124 | // Clear scheduled cron jobs 125 | wp_clear_scheduled_hook('aisprtsw_cron'); 126 | wp_clear_scheduled_hook('aisprtsw_fetch_cron'); 127 | 128 | $this->log('Plugin deactivated'); 129 | } 130 | 131 | /** 132 | * Initialize admin pages 133 | */ 134 | private function initializePages( 135 | PostConfigPage $postConfigPage = null, 136 | CronSettingsPage $cronSettingsPage = null, 137 | ApiConfigPage $apiConfigPage = null 138 | ): void { 139 | $this->postConfigPage = $postConfigPage ?? new PostConfigPage(); 140 | $this->cronSettingsPage = $cronSettingsPage ?? new CronSettingsPage(); 141 | $this->apiConfigPage = $apiConfigPage ?? new ApiConfigPage(); 142 | } 143 | 144 | /** 145 | * Setup WordPress hooks 146 | */ 147 | private function setupHooks(): void 148 | { 149 | add_action('init', [$this, 'registerPages']); 150 | add_action('admin_menu', [$this, 'addMainPluginMenu']); 151 | add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']); 152 | } 153 | 154 | /** 155 | * Enqueue admin scripts and styles 156 | */ 157 | public function enqueueAdminScripts($hook): void 158 | { 159 | // Only enqueue on plugin pages 160 | if (strpos($hook, 'ai-sports-writer') === false) { 161 | return; 162 | } 163 | 164 | wp_enqueue_media(); 165 | wp_enqueue_script( 166 | 'ai-sports-writer-js', 167 | plugin_dir_url(AISPRTSW_PLUGIN_FILE) . 'assets/js/ai-sports-writer.js', 168 | ['jquery'], 169 | self::PLUGIN_VERSION, 170 | true 171 | ); 172 | 173 | // Add localized variables to the JS file 174 | wp_localize_script( 175 | 'ai-sports-writer-js', 176 | 'fcg_ajax_object', 177 | [ 178 | 'ajax_url' => admin_url('admin-ajax.php'), 179 | 'nonce' => wp_create_nonce('fcg_nonce') 180 | ] 181 | ); 182 | } 183 | 184 | 185 | /** 186 | * Register admin pages 187 | */ 188 | public function registerPages(): void 189 | { 190 | $this->postConfigPage->register(); 191 | $this->cronSettingsPage->register(); 192 | $this->apiConfigPage->register(); 193 | } 194 | 195 | /** 196 | * Add main plugin menu and submenus 197 | */ 198 | public function addMainPluginMenu(): void 199 | { 200 | if (!current_user_can('manage_options')) { 201 | return; 202 | } 203 | 204 | add_menu_page( 205 | __('AI Sports Writer', 'ai-sports-writer'), 206 | __('AI Sports Writer', 'ai-sports-writer'), 207 | 'manage_options', 208 | self::MENU_SLUG, 209 | [$this, 'renderMainPage'], 210 | 'dashicons-admin-site-alt3', 211 | 20 212 | ); 213 | 214 | add_submenu_page( 215 | self::MENU_SLUG, 216 | __('Post Settings', 'ai-sports-writer'), 217 | __('Post Config', 'ai-sports-writer'), 218 | 'manage_options', 219 | self::POST_SETTINGS_SLUG, 220 | [$this, 'post_settings_page'] 221 | ); 222 | 223 | add_submenu_page( 224 | self::MENU_SLUG, 225 | __('Cron Settings', 'ai-sports-writer'), 226 | __('Cron status', 'ai-sports-writer'), 227 | 'manage_options', 228 | self::CRON_SETTINGS_SLUG, 229 | [$this, 'post_settings_cron'] 230 | ); 231 | } 232 | 233 | public static function initialize_content_generator() 234 | { 235 | $sport_api_service = new SportApiService(); 236 | $openai_service = new OpenAiService(); 237 | 238 | $content_generator = new ContentGenerator($sport_api_service, $openai_service); 239 | } 240 | 241 | /** 242 | * Render main plugin page 243 | */ 244 | public function renderMainPage(): void 245 | { 246 | ?> 247 |
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/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 | 'dalle_image_size' => '1024x1024', 54 | 'dalle_image_quality' => 'standard', 55 | 'openai_model' => 'gpt-3.5-turbo', 56 | ], 57 | ] 58 | ); 59 | 60 | // Post Configuration Section 61 | add_settings_section( 62 | 'post_settings', 63 | '', 64 | [$this, 'render_post_settings_section'], 65 | 'ai-sports-writer-post' 66 | ); 67 | 68 | add_settings_field( 69 | 'max_games_per_day', 70 | 'Maximum Games Per Day', 71 | [$this, 'render_max_games_per_day_field'], 72 | 'ai-sports-writer-post', 73 | 'post_settings' 74 | ); 75 | 76 | add_settings_field( 77 | 'max_games_per_hour', 78 | 'Maximum Games Per Hour', 79 | [$this, 'render_max_games_per_hour_field'], 80 | 'ai-sports-writer-post', 81 | 'post_settings' 82 | ); 83 | 84 | add_settings_field( 85 | 'post_intervals', 86 | 'Post Intervals (minutes)', 87 | [$this, 'render_post_intervals_field'], 88 | 'ai-sports-writer-post', 89 | 'post_settings' 90 | ); 91 | 92 | add_settings_field( 93 | 'post_author', 94 | 'Default Post Author', 95 | [$this, 'render_post_author_field'], 96 | 'ai-sports-writer-post', 97 | 'post_settings' 98 | ); 99 | 100 | add_settings_field( 101 | 'post_category', 102 | 'Default Post Category', 103 | [$this, 'render_post_category_field'], 104 | 'ai-sports-writer-post', 105 | 'post_settings' 106 | ); 107 | 108 | 109 | // Prompt Configuration Section 110 | add_settings_section( 111 | 'prompt_settings', 112 | 'AI Prompt Configuration', 113 | [$this, 'render_prompt_settings_section'], 114 | 'ai-sports-writer-post' 115 | ); 116 | 117 | add_settings_field( 118 | 'ai_content_prompt', 119 | 'AI Content Generation Prompt', 120 | [$this, 'render_ai_content_prompt_field'], 121 | 'ai-sports-writer-post', 122 | 'prompt_settings' 123 | ); 124 | 125 | 126 | 127 | // Featured Image Section 128 | add_settings_section( 129 | 'image_settings', 130 | 'Featured Image Settings', 131 | [$this, 'render_image_settings_section'], 132 | 'ai-sports-writer-post' 133 | ); 134 | 135 | add_settings_field( 136 | 'featured_image_upload', 137 | 'Featured Image', 138 | [$this, 'render_featured_image_upload_field'], 139 | 'ai-sports-writer-post', 140 | 'image_settings' 141 | ); 142 | 143 | add_settings_field( 144 | 'dalle_image_generation', 145 | 'DALL-E Image Generation', 146 | [$this, 'render_dalle_image_generation_field'], 147 | 'ai-sports-writer-post', 148 | 'image_settings' 149 | ); 150 | 151 | add_settings_field( 152 | 'dalle_image_size', 153 | 'DALL-E Image Size', 154 | [$this, 'render_dalle_image_size_field'], 155 | 'ai-sports-writer-post', 156 | 'image_settings' 157 | ); 158 | 159 | add_settings_field( 160 | 'dalle_image_quality', 161 | 'DALL-E Image Quality', 162 | [$this, 'render_dalle_image_quality_field'], 163 | 'ai-sports-writer-post', 164 | 'image_settings' 165 | ); 166 | } 167 | 168 | 169 | // Render callback for post settings 170 | public function render_post_settings_section(): void 171 | { 172 | echo '

' . esc_html__('Configure post generation settings such as the maximum number of posts per day/hour.', 'ai-sports-writer') . '

'; 173 | } 174 | 175 | // Render field for maximum games per day 176 | public function render_max_games_per_day_field(): void 177 | { 178 | $options = get_option('aisprtsw_post_settings'); 179 | $value = $options['max_games_per_day'] ?? 5; 180 | echo ''; 181 | } 182 | 183 | // Render field for maximum games per hour 184 | public function render_max_games_per_hour_field(): void 185 | { 186 | $options = get_option('aisprtsw_post_settings'); 187 | $value = $options['max_games_per_hour'] ?? 5; 188 | echo ''; 189 | } 190 | 191 | // Render field for post intervals (in minutes) 192 | public function render_post_intervals_field(): void 193 | { 194 | $options = get_option('aisprtsw_post_settings'); 195 | $value = $options['post_intervals'] ?? 5; 196 | echo ''; 197 | echo '

Interval in minutes between scheduled posts (1-30 minutes).

'; 198 | } 199 | 200 | // Render field for default post author 201 | public function render_post_author_field(): void 202 | { 203 | $options = get_option('aisprtsw_post_settings'); 204 | $selected = $options['post_author'] ?? get_current_user_id(); 205 | 206 | $users = get_users([ 207 | 'capability' => 'publish_posts', 208 | 'fields' => ['ID', 'display_name'], 209 | ]); 210 | 211 | echo ''; 219 | echo '

Select the default author for automatically generated posts.

'; 220 | } 221 | 222 | 223 | 224 | // Render field for default post category 225 | public function render_post_category_field(): void 226 | { 227 | $options = get_option('aisprtsw_post_settings'); 228 | $selected = $options['post_category'] ?? 0; 229 | 230 | $categories = get_categories(['hide_empty' => false]); 231 | 232 | echo ''; 246 | echo '

Select the default category for automatically generated posts.

'; 247 | } 248 | 249 | 250 | public function render_prompt_settings_section(): void {} 251 | 252 | //Render field for AI content generation prompt 253 | public function render_ai_content_prompt_field(): void 254 | { 255 | $options = get_option('aisprtsw_post_settings'); 256 | $value = $options['ai_content_prompt'] ?? ''; 257 | 258 | echo ''; 259 | echo '

Customize the prompt sent to OpenAI for content generation.

'; 260 | } 261 | 262 | 263 | // Render callback for image settings 264 | public function render_image_settings_section(): void 265 | { 266 | echo '

Set the configuration for featured images in posts.

'; 267 | } 268 | // Render field for uploading featured image 269 | public function render_featured_image_upload_field(): void 270 | { 271 | $options = get_option('aisprtsw_post_settings'); 272 | $value = $options['featured_image_url'] ?? ''; 273 | 274 | echo ''; 275 | echo ''; 276 | 277 | $attachment_id = attachment_url_to_postid($value); 278 | 279 | if ($attachment_id) { 280 | // Display the image using wp_get_attachment_image 281 | echo '
'; 282 | echo wp_get_attachment_image($attachment_id, 'medium'); 283 | echo '
'; 284 | } 285 | } 286 | 287 | // Render checkbox for DALL-E image generation 288 | public function render_dalle_image_generation_field(): void 289 | { 290 | $options = get_option('aisprtsw_post_settings'); 291 | $checked = checked(1, ($options['dalle_image_generation'] ?? 0), false); 292 | 293 | echo ''; 297 | echo '

When checked, the plugin will attempt to generate a featured image using DALL-E for each post.

'; 298 | } 299 | 300 | // Render dropdown for DALL-E image size 301 | public function render_dalle_image_size_field(): void 302 | { 303 | $options = get_option('aisprtsw_post_settings'); 304 | $current_size = $options['dalle_image_size'] ?? '1024x1024'; 305 | 306 | $size_options = [ 307 | '1024x1024' => 'Square (1024x1024) - Classic format', 308 | '1792x1024' => 'Landscape (1792x1024) - Stadium scenes', 309 | '1024x1792' => 'Portrait (1024x1792) - Social media' 310 | ]; 311 | 312 | echo ''; 323 | echo '

Choose the aspect ratio that best fits your content layout.

'; 324 | } 325 | 326 | // Render dropdown for DALL-E image quality 327 | public function render_dalle_image_quality_field(): void 328 | { 329 | $options = get_option('aisprtsw_post_settings'); 330 | $current_quality = $options['dalle_image_quality'] ?? 'standard'; 331 | 332 | $quality_options = [ 333 | 'standard' => 'Standard - Faster generation, lower cost', 334 | 'hd' => 'HD - Higher detail, premium quality' 335 | ]; 336 | 337 | echo ''; 348 | echo '

HD quality provides more detailed images but costs more to generate.

'; 349 | } 350 | 351 | 352 | // Sanitize input settings before saving 353 | public function sanitize_settings($input): array 354 | { 355 | $sanitized = []; 356 | 357 | // Max games per day validation 358 | if ($input['max_games_per_day'] < 1 || $input['max_games_per_day'] > 100) { 359 | add_settings_error( 360 | 'aisprtsw_post_settings', 361 | 'invalid_max_games_per_day', 362 | __('Maximum games per day should be between 1 and 100.', 'ai-sports-writer') 363 | ); 364 | $sanitized['max_games_per_day'] = 5; 365 | } else { 366 | $sanitized['max_games_per_day'] = (int) $input['max_games_per_day']; 367 | } 368 | 369 | 370 | // Max games per hour validation 371 | if ($input['max_games_per_hour'] < 1 || $input['max_games_per_hour'] > 24) { 372 | add_settings_error( 373 | 'aisprtsw_post_settings', 374 | 'invalid_max_games_per_hour', 375 | __('Maximum games per hour should be between 1 and 24.', 'ai-sports-writer') 376 | ); 377 | $sanitized['max_games_per_hour'] = 5; 378 | } else { 379 | $sanitized['max_games_per_hour'] = (int) $input['max_games_per_hour']; 380 | } 381 | 382 | // Post intervals validation 383 | $post_intervals = (int) ($input['post_intervals'] ?? 5); 384 | if ($post_intervals < 1 || $post_intervals > 30) { 385 | add_settings_error( 386 | 'aisprtsw_post_settings', 387 | 'invalid_post_intervals', 388 | __('Post intervals should be between 1 and 30 minutes.', 'ai-sports-writer') 389 | ); 390 | $sanitized['post_intervals'] = 5; 391 | } else { 392 | $sanitized['post_intervals'] = $post_intervals; 393 | } 394 | 395 | // Featured image url validator 396 | $featured_image_url = esc_url_raw($input['featured_image_url'] ?? ''); 397 | if (!empty($featured_image_url)) { 398 | $file_type = wp_check_filetype($featured_image_url); 399 | $allowed_image_types = ['jpg', 'jpeg', 'png', 'gif']; 400 | 401 | if (!filter_var($featured_image_url, FILTER_VALIDATE_URL)) { 402 | add_settings_error( 403 | 'aisprtsw_post_settings', 404 | 'invalid_featured_image_url', 405 | __('The featured image URL is not a valid URL.', 'ai-sports-writer') 406 | ); 407 | $sanitized['featured_image_url'] = ''; 408 | } elseif (!in_array($file_type['ext'], $allowed_image_types)) { 409 | add_settings_error( 410 | 'aisprtsw_post_settings', 411 | 'invalid_featured_image_type', 412 | __('The featured image URL does not point to a valid image file (jpg, jpeg, png, or gif).', 'ai-sports-writer') 413 | ); 414 | $sanitized['featured_image_url'] = ''; 415 | } else { 416 | $sanitized['featured_image_url'] = $featured_image_url; 417 | } 418 | } else { 419 | $sanitized['featured_image_url'] = ''; 420 | } 421 | 422 | $sanitized['dalle_image_generation'] = (int) ($input['dalle_image_generation'] ?? 0); 423 | 424 | // DALL-E image size validation 425 | $allowed_sizes = ['1024x1024', '1792x1024', '1024x1792']; 426 | $sanitized['dalle_image_size'] = in_array($input['dalle_image_size'] ?? '', $allowed_sizes, true) 427 | ? $input['dalle_image_size'] 428 | : '1024x1024'; 429 | 430 | // DALL-E image quality validation 431 | $allowed_qualities = ['standard', 'hd']; 432 | $sanitized['dalle_image_quality'] = in_array($input['dalle_image_quality'] ?? '', $allowed_qualities, true) 433 | ? $input['dalle_image_quality'] 434 | : 'standard'; 435 | 436 | $sanitized['post_author'] = (int) ($input['post_author'] ?? get_current_user_id()); 437 | $sanitized['post_category'] = (int) ($input['post_category'] ?? 0); 438 | 439 | $allowed_html = [ 440 | 'p' => [], 441 | 'br' => [], 442 | 'strong' => [], 443 | 'em' => [], 444 | 'ul' => [], 445 | 'ol' => [], 446 | 'li' => [], 447 | ]; 448 | 449 | $sanitized['ai_content_prompt'] = wp_kses($input['ai_content_prompt'] ?? '', $allowed_html); 450 | $allowed_models = [ 451 | 'gpt-3.5-turbo', 452 | 'gpt-4o-mini', 453 | 'gpt-4-turbo', 454 | 'gpt-4o', 455 | 'gpt-4o-2024-08-06', 456 | 'o1-mini' 457 | ]; 458 | $sanitized['openai_model'] = in_array($input['openai_model'] ?? '', $allowed_models, true) 459 | ? $input['openai_model'] 460 | : 'gpt-3.5-turbo'; 461 | 462 | 463 | return $sanitized; 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /includes/Core/ContentGenerator.php: -------------------------------------------------------------------------------- 1 | sport_api_service = $sport_api_service; 20 | $this->openai_service = $openai_service; 21 | $this->setup_cron(); 22 | } 23 | 24 | /** 25 | * Set up cron jobs for content generation. 26 | */ 27 | private function setup_cron(): void 28 | { 29 | add_filter('cron_schedules', [$this, 'register_custom_intervals']); 30 | 31 | // Schedule events if not already scheduled 32 | if (!wp_next_scheduled('aisprtsw_fetch_cron')) { 33 | wp_schedule_event(time(), 'every_three_hours', 'aisprtsw_fetch_cron'); 34 | } 35 | if (!wp_next_scheduled('aisprtsw_cron')) { 36 | wp_schedule_event(time(), 'ten_minutes_before_hour', 'aisprtsw_cron'); 37 | } 38 | 39 | add_action('aisprtsw_fetch_cron', array($this, 'run_upcoming_games')); 40 | add_action('aisprtsw_cron', array($this, 'run_content_generation')); 41 | } 42 | 43 | /** 44 | * Register custom cron intervals. 45 | * 46 | * @param array $schedules Existing cron schedules. 47 | * @return array Modified cron schedules. 48 | */ 49 | public function register_custom_intervals(array $schedules): array 50 | { 51 | $schedules['every_three_hours'] = [ 52 | 'interval' => 3 * HOUR_IN_SECONDS, 53 | 'display' => __('Every 3 Hours', 'ai-sports-writer') 54 | ]; 55 | $schedules['ten_minutes_before_hour'] = [ 56 | 'interval' => 3600 - 600, // 1 hour - 10 minutes 57 | 'display' => __('10 Minutes Before Hour', 'ai-sports-writer') 58 | ]; 59 | return $schedules; 60 | } 61 | 62 | 63 | public function run_upcoming_games(): void 64 | { 65 | $api_options = get_option(self::OPTION_API_NAME); 66 | $api_key = $api_options['sport_api_key'] ?? ''; 67 | 68 | if (empty($api_key)) { 69 | Logger::log('Sport API key not found.', 'error'); 70 | return; 71 | } 72 | 73 | $games = $this->sport_api_service->fetchUpcomingEndpoint($api_key); 74 | if ($games) { 75 | $this->insert_games_into_db($games); 76 | } else { 77 | Logger::log('Failed to fetch games data.', 'error'); 78 | } 79 | } 80 | 81 | 82 | 83 | private function insert_games_into_db(array $games): void 84 | { 85 | global $wpdb; 86 | $table_name = $wpdb->prefix . 'football_games'; 87 | $two_days_ago = gmdate('Y-m-d H:i:s', strtotime('-2 days')); 88 | 89 | //deleting old games 90 | $where = [ 91 | 'match_datetime' => $two_days_ago 92 | ]; 93 | $deleted_count = $wpdb->delete($table_name, $where, ['%s']); 94 | 95 | $inserted_count = 0; 96 | foreach ($games['data'] as $game) { 97 | // Check for existing match 98 | $existing_match = $wpdb->get_var( 99 | $wpdb->prepare( 100 | "SELECT COUNT(*) FROM {$table_name} WHERE match_code = %s", 101 | $game['match_code'] 102 | ) 103 | ); 104 | 105 | if ($existing_match > 0) { 106 | continue; 107 | } 108 | 109 | // Prepare data for insertion 110 | $data = [ 111 | 'match_code' => $game['match_code'] ?? null, 112 | 'region' => $game['region'] ?? '', 113 | 'team' => $game['team'] ?? '', 114 | 'home' => $game['home'] ?? '', 115 | 'away' => $game['away'] ?? '', 116 | 'match_datetime' => $game['match_datetime'] ?? null, 117 | 'time_zone' => $game['time_zone'] ?? '', 118 | 'provider' => $game['provider'] ?? '', 119 | 'odds' => wp_json_encode($game['odds'] ?? []), 120 | ]; 121 | 122 | // Formats for each field to ensure proper sanitization 123 | $formats = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']; 124 | 125 | // Insert the game data 126 | $result = $wpdb->insert($table_name, $data, $formats); 127 | if ($result) { 128 | $inserted_count++; 129 | } 130 | } 131 | 132 | Logger::log("Games processed: " . count($games['data']) . ", inserted: $inserted_count"); 133 | } 134 | 135 | 136 | 137 | 138 | 139 | public function run_content_generation(): void 140 | { 141 | $api_options = get_option(self::OPTION_API_NAME); 142 | $post_options = get_option(self::OPTION_POST_NAME); 143 | 144 | 145 | $openai_api_key = $api_options['openai_api_key'] ?? ''; 146 | $sport_api_key = $api_options['sport_api_key'] ?? ''; 147 | 148 | if (empty($openai_api_key) || empty($sport_api_key)) { 149 | Logger::log('API keys not found.', 'error'); 150 | return; 151 | } 152 | 153 | 154 | $max_games_per_day = (int)($post_options['max_games_per_day'] ?? 5); 155 | $max_games_per_hour = (int)($post_options['max_games_per_hour'] ?? 5); 156 | $post_interval = (int)($post_options['post_intervals'] ?? 5); 157 | $ai_content_prompt = $post_options['ai_content_prompt'] ?? ''; 158 | 159 | global $wpdb; 160 | $table_name = $wpdb->prefix . 'football_games'; 161 | $today = gmdate('Y-m-d'); 162 | 163 | 164 | $games_processed_today = $wpdb->get_var( 165 | $wpdb->prepare( 166 | "SELECT COUNT(*) FROM `{$wpdb->prefix}football_games` WHERE processed = 1 AND DATE(processed_started_at) = %s", 167 | $today 168 | ) 169 | ); 170 | 171 | if ($games_processed_today >= $max_games_per_day) { 172 | Logger::log("Max games per day reached ($games_processed_today).", 'warning'); 173 | return; 174 | } 175 | 176 | 177 | 178 | $current_time = current_time('mysql'); 179 | $games = $wpdb->get_results( 180 | $wpdb->prepare( 181 | "SELECT * FROM {$wpdb->prefix}football_games WHERE processed = 0 AND match_datetime > %s LIMIT %d", 182 | $current_time, 183 | $max_games_per_hour 184 | ), 185 | ARRAY_A 186 | ); 187 | 188 | if (!$games) { 189 | Logger::log('No unprocessed games found.', 'notice'); 190 | return; 191 | } 192 | 193 | 194 | foreach ($games as $index => $game) { 195 | try { 196 | $wpdb->update( 197 | $table_name, 198 | ['processed' => 1, 'processed_started_at' => current_time('mysql')], 199 | ['id' => $game['id']] 200 | ); 201 | 202 | $all_stats = $this->sport_api_service->fetchGameStatistics($sport_api_key, $game['match_code']); 203 | 204 | if ($all_stats === null) { 205 | // Handle error, e.g., log, and skip to the next game. 206 | Logger::log('Failed to fetch stats for match code: ' . $game['match_code'] . '.', 'error'); 207 | 208 | // Mark game processing as failed or skipped if needed. 209 | $wpdb->update( 210 | $table_name, 211 | ['processed' => 2, 'processed_failed_at' => current_time('mysql')], 212 | ['id' => $game['id']] 213 | ); 214 | continue; 215 | } 216 | 217 | 218 | $prompt = $this->prepare_content_prompt($game, $all_stats, $ai_content_prompt); 219 | $ai_content = $this->openai_service->generateContent($openai_api_key, $prompt); 220 | 221 | $post_time = gmdate('Y-m-d H:i:s', strtotime("+1 hour +" . ($index * $post_interval) . " minutes")); 222 | 223 | if ($ai_content) { 224 | $this->schedule_content_post($ai_content, $post_time, $game); 225 | } else { 226 | Logger::log('Failed to generate content.', 'error'); 227 | } 228 | $wpdb->update($table_name, ['process_completed_at' => current_time('mysql')], ['id' => $game['id']]); 229 | } catch (\Throwable $th) { 230 | Logger::log("An error occurred during game processing: " . $th->getMessage(), 'error'); 231 | 232 | continue; // Skip to the next game 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Prepare content prompt with game details 239 | * 240 | * @param array $game Game data 241 | * @param array $all_stats Game statistics 242 | * @param string $base_prompt Initial content prompt 243 | * @return string Prepared content prompt 244 | */ 245 | private function prepare_content_prompt( 246 | array $game, 247 | array $all_stats, 248 | string $base_prompt 249 | ): string { 250 | 251 | $home_recent_matches = ""; 252 | $away_recent_matches = ""; 253 | $head_to_head_matches = ""; 254 | 255 | if (isset($all_stats["home_matches"]) && is_array($all_stats["home_matches"])) { 256 | $home_recent_matches = implode( 257 | "\n", 258 | array_map( 259 | function ($match) { 260 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 261 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 262 | : ""; 263 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 264 | $halfTime . 265 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 266 | ". Date: " . $match["match_date"]; 267 | }, 268 | array_reverse($all_stats["home_matches"]) //display recent history first 269 | ) 270 | ); 271 | } 272 | 273 | 274 | 275 | 276 | if (isset($all_stats["away_matches"]) && is_array($all_stats["away_matches"])) { 277 | $away_recent_matches = implode( 278 | "\n", 279 | array_map( 280 | function ($match) { 281 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 282 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 283 | : ""; 284 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 285 | $halfTime . 286 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 287 | ". Date: " . $match["match_date"]; 288 | }, 289 | array_reverse($all_stats["away_matches"]) //display recent history first 290 | ) 291 | ); 292 | } 293 | 294 | 295 | 296 | if (isset($all_stats["head_to_head"]) && is_array($all_stats["head_to_head"])) { 297 | $head_to_head_matches = implode( 298 | "\n", 299 | array_map( 300 | function ($match) { 301 | $halfTime = isset($match["home_ht_score"], $match["away_ht_score"]) 302 | ? ". Half time: " . $match["home_ht_score"] . ":" . $match["away_ht_score"] 303 | : ""; 304 | return "-" . $match["home_team_name"] . " vs " . $match["away_team_name"] . 305 | $halfTime . 306 | ". Full time: " . $match["home_ft_score"] . ":" . $match["away_ft_score"] . 307 | ". Date: " . $match["match_date"]; 308 | }, 309 | array_reverse($all_stats["head_to_head"]) //display recent history first 310 | ) 311 | ); 312 | } 313 | 314 | 315 | 316 | 317 | $oddsBreakdown = ""; 318 | $game["odds"] = json_decode($game["odds"], true); 319 | 320 | if (!empty($game["odds"])) { 321 | $oddsBreakdown = implode("\n", [ 322 | "\nBetting Odds Breakdown:", 323 | "- Home Win ({$game["home"]}): {$game["odds"]["1"]}", 324 | "- Away Win ({$game["away"]}): {$game["odds"]["2"]}", 325 | "- Either team to Win: {$game["odds"]["12"]}", 326 | "- Draw: {$game["odds"]["x"]}", 327 | "- Home win or draw: {$game["odds"]["1x"]}", 328 | "- Away win or draw: {$game["odds"]["x2"]}", 329 | "- Total goals, less than 3 goals: {$game["odds"]["u_2_5"]}", 330 | "- Total goals, 3 goals or more: {$game["odds"]["o_2_5"]}", 331 | ]); 332 | } 333 | 334 | 335 | $sections = [ 336 | "\n\nMatch Details:", 337 | "- Upcoming Match: {$game["home"]} vs {$game["away"]}", 338 | "- Home team: {$game["home"]}", 339 | "- Away team: {$game["away"]}", 340 | "- Match Date: {$game["match_datetime"]}", 341 | "- Region: {$game["region"]}", 342 | "", 343 | $oddsBreakdown, 344 | "", 345 | !empty($home_recent_matches) ? "\nMatch History Analysis:\nHome Team Recent Performance:\n$home_recent_matches" : null, 346 | !empty($away_recent_matches) ? "\nAway Team Recent Performance:\n$away_recent_matches" : null, 347 | !empty($head_to_head_matches) ? "\nHead-to-Head History:\n$head_to_head_matches" : null, 348 | ]; 349 | 350 | // Remove null or empty values 351 | $sections = array_filter($sections); 352 | 353 | // Create the final prompt 354 | $base_prompt .= implode("\n", $sections); 355 | 356 | return $base_prompt; 357 | } 358 | 359 | /** 360 | * Schedules a post with content, title, and featured image, and assigns it a future publish date. 361 | * 362 | * @param string $content The content of the post to be scheduled. 363 | * @param string $post_time The scheduled time for the post to be published. 364 | * @param object $game Game data that may be used to generate a DALL-E image. 365 | * 366 | * @return int|void The post ID of the created post or void if an error occurs. 367 | */ 368 | private function schedule_content_post($content, $post_time, $game) 369 | { 370 | 371 | $api_options = get_option(self::OPTION_API_NAME); 372 | $post_options = get_option(self::OPTION_POST_NAME); 373 | 374 | // Validate API keys 375 | $openai_api_key = $api_options['openai_api_key'] ?? ''; 376 | $sport_api_key = $api_options['sport_api_key'] ?? ''; 377 | 378 | if (empty($openai_api_key) || empty($sport_api_key)) { 379 | Logger::log('Missing API keys for content generation', 'ERROR'); 380 | return; 381 | } 382 | 383 | $openai_title = $this->openai_service->generateTitle($content, $openai_api_key); 384 | 385 | // If OpenAI title is empty or returns an error, we use the fallback method 386 | $post_title = !empty($openai_title) ? $openai_title : $this->generate_post_title($content); 387 | 388 | $post_data = [ 389 | 'post_title' => $post_title, 390 | 'post_content' => $content, 391 | 'post_status' => 'future', 392 | 'post_date' => $post_time, 393 | 'post_type' => 'post', 394 | 'post_author' => isset($post_options['post_author']) ? $post_options['post_author'] : get_current_user_id(), 395 | ]; 396 | 397 | $post_id = wp_insert_post($post_data); 398 | if (is_wp_error($post_id)) { 399 | Logger::log('Failed to insert post', 'ERROR'); 400 | return; 401 | } 402 | 403 | // Log the successful post creation 404 | // Logger::log("Post created successfully: ID {$post_id}", 'INFO'); 405 | 406 | // Handle categories 407 | if (isset($post_options['post_category']) && $post_options['post_category'] > 0) { 408 | wp_set_post_categories($post_id, [$post_options['post_category']], false); 409 | } 410 | 411 | // Handle featured image 412 | if ($post_id) { 413 | $manual_image = isset($post_options['featured_image_url']) ? $post_options['featured_image_url'] : ''; 414 | $dalle_enabled = isset($post_options['dalle_image_generation']) && $post_options['dalle_image_generation'] == 1; 415 | 416 | $featured_image_url = $dalle_image = null; 417 | if (!empty($manual_image)) { 418 | $featured_image_url = $manual_image; 419 | } 420 | 421 | if ($dalle_enabled) { 422 | $dalle_image = $this->openai_service->generateDalleImage($openai_api_key, $game); 423 | if ($dalle_image) { 424 | $featured_image_url = $dalle_image; 425 | } 426 | } 427 | 428 | // Upload and set featured image if URL is available 429 | if ($featured_image_url) { 430 | require_once ABSPATH . 'wp-admin/includes/file.php'; 431 | require_once ABSPATH . 'wp-admin/includes/media.php'; 432 | require_once ABSPATH . 'wp-admin/includes/image.php'; 433 | 434 | // Download the image from the URL 435 | $tmp_file = download_url($featured_image_url, 60); 436 | 437 | if (is_wp_error($tmp_file)) { 438 | Logger::log("Failed to download image from URL: {$featured_image_url}. Error: " . $tmp_file->get_error_message(), 'ERROR'); 439 | return; 440 | } 441 | 442 | // Get the mime type of the downloaded file 443 | $file_type = wp_check_filetype($tmp_file, null); 444 | 445 | // Note: DALL-E responses currently don't include file extensions in their metadata, 446 | // so we manually append .png as fallback if no extension is provided 447 | // Generate random filename with the correct extension 448 | $random_filename = 'image-' . uniqid() . (empty($file_type['ext']) ? '.png' : '.' . $file_type['ext']); 449 | 450 | 451 | // Prepare the file array for media_handle_sideload 452 | $file = [ 453 | 'name' => $random_filename, 454 | 'tmp_name' => $tmp_file, 455 | ]; 456 | 457 | // Use media_handle_sideload to upload the file 458 | $attachment_id = media_handle_sideload($file, $post_id); 459 | 460 | // Check for upload errors 461 | if (is_wp_error($attachment_id)) { 462 | wp_delete_file($tmp_file); // Remove the temporary file 463 | Logger::log("Failed to upload and sideload image. Error: " . $attachment_id->get_error_message(), 'ERROR'); 464 | return; 465 | } 466 | 467 | // Set the uploaded image as the featured image 468 | set_post_thumbnail($post_id, $attachment_id); 469 | 470 | // Log the successful image upload 471 | Logger::log("Featured image set successfully for post ID {$post_id}", 'INFO'); 472 | } 473 | } 474 | 475 | return $post_id; 476 | } 477 | 478 | 479 | 480 | /** 481 | * Generate a basic post title from the given content. 482 | * 483 | * @param string $content The content from which to generate the title. 484 | * @return string The generated post title. 485 | */ 486 | private function generate_post_title($content) 487 | { 488 | // Basic title generation from content 489 | $words = wp_trim_words($content, 6, '...'); 490 | return $words; 491 | } 492 | } 493 | --------------------------------------------------------------------------------