PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.4
3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / rest.php
ai-engine / classes Last commit date
data 1 year ago engines 8 months ago exceptions 1 year ago modules 8 months ago query 8 months ago rest 9 months ago services 8 months ago admin.php 8 months ago api.php 8 months ago core.php 8 months ago discussion.php 1 year ago event.php 1 year ago init.php 8 months ago logging.php 1 year ago reply.php 8 months ago rest.php 8 months ago
rest.php
2657 lines
1 <?php
2
3 class Meow_MWAI_Rest {
4 private $core = null;
5 private $namespace = 'mwai/v1';
6
7 public function __construct( $core ) {
8 $this->core = $core;
9 add_action( 'rest_api_init', [ $this, 'rest_init' ] );
10 }
11
12 /**
13 * Retrieve the message from the parameters and optionally sanitize it.
14 *
15 * @param array &$params The parameters array, passed by reference.
16 * @param bool $sanitize Whether to sanitize the message using sanitize_text_field.
17 * @return string The retrieved (and optionally sanitized) message.
18 */
19 public function retrieve_message( &$params, $sanitize = false ): string {
20 if ( isset( $params['message'] ) ) {
21 $message = $params['message'];
22 }
23 elseif ( isset( $params['prompt'] ) ) {
24 $message = $params['prompt'];
25 unset( $params['prompt'] );
26 $params['message'] = $message;
27 Meow_MWAI_Logging::deprecated( '"prompt" is deprecated, please use "message" instead.' );
28 }
29 else {
30 $message = '';
31 }
32
33 if ( $sanitize ) {
34 $message = sanitize_text_field( $message );
35 }
36
37 return $message;
38 }
39
40 /**
41 * Helper method to create REST responses with automatic token refresh
42 *
43 * @param array $data The response data
44 * @param int $status HTTP status code
45 * @return WP_REST_Response
46 */
47 protected function create_rest_response( $data, $status = 200 ) {
48 // Always check if we need to provide a new nonce
49 $current_nonce = $this->core->get_nonce( true );
50 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
51
52 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
53 // We'll refresh if the nonce is older than 10 hours to be safe
54 $should_refresh = false;
55
56 if ( $request_nonce ) {
57 // Try to determine the age of the nonce
58 // WordPress uses a tick system where each tick is 12 hours
59 // If we're in the second half of the nonce's life, refresh it
60 $time = time();
61 $nonce_tick = wp_nonce_tick();
62
63 // Verify if the nonce is still valid but getting old
64 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
65 if ( $verify === 2 ) {
66 // Nonce is valid but was generated 12-24 hours ago
67 $should_refresh = true;
68 // Log will be written when token is included in response
69 }
70 }
71
72 // If the nonce has changed or should be refreshed, include the new one
73 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
74 $data['new_token'] = $current_nonce;
75
76 // Log if server debug mode is enabled
77 if ( $this->core->get_option( 'server_debug_mode' ) ) {
78 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
79 }
80 }
81
82 return new WP_REST_Response( $data, $status );
83 }
84
85 public function rest_init() {
86 try {
87 // Session Endpoint
88 register_rest_route( $this->namespace, '/start_session', [
89 'methods' => 'POST',
90 'permission_callback' => '__return_true', // Public endpoint for guest users
91 'callback' => [ $this, 'rest_start_session' ],
92 ] );
93
94 // Settings Endpoints
95 register_rest_route( $this->namespace, '/settings/update', [
96 'methods' => 'POST',
97 'permission_callback' => [ $this->core, 'can_access_settings' ],
98 'callback' => [ $this, 'rest_settings_update' ],
99 ] );
100 register_rest_route( $this->namespace, '/settings/options', [
101 'methods' => 'GET',
102 'permission_callback' => [ $this->core, 'can_access_settings' ],
103 'callback' => [ $this, 'rest_settings_list' ],
104 ] );
105 register_rest_route( $this->namespace, '/settings/reset', [
106 'methods' => 'POST',
107 'permission_callback' => [ $this->core, 'can_access_settings' ],
108 'callback' => [ $this, 'rest_settings_reset' ],
109 ] );
110 register_rest_route( $this->namespace, '/settings/chatbots', [
111 'methods' => ['GET', 'POST'],
112 'permission_callback' => [ $this->core, 'can_access_settings' ],
113 'callback' => [ $this, 'rest_settings_chatbots' ],
114 ] );
115 register_rest_route( $this->namespace, '/settings/themes', [
116 'methods' => ['GET', 'POST'],
117 'permission_callback' => [ $this->core, 'can_access_settings' ],
118 'callback' => [ $this, 'rest_settings_themes' ],
119 ] );
120
121 // System Endpoints
122 register_rest_route( $this->namespace, '/system/logs/list', [
123 'methods' => 'POST',
124 'permission_callback' => [ $this->core, 'can_access_settings' ],
125 'callback' => [ $this, 'rest_system_logs_list' ],
126 ] );
127 register_rest_route( $this->namespace, '/system/logs/delete', [
128 'methods' => 'POST',
129 'permission_callback' => [ $this->core, 'can_access_settings' ],
130 'callback' => [ $this, 'rest_system_logs_delete' ],
131 ] );
132 register_rest_route( $this->namespace, '/system/logs/meta', [
133 'methods' => 'POST',
134 'permission_callback' => [ $this->core, 'can_access_settings' ],
135 'callback' => [ $this, 'rest_system_logs_meta_get' ],
136 ] );
137 register_rest_route( $this->namespace, '/system/logs/activity', [
138 'methods' => 'POST',
139 'permission_callback' => [ $this->core, 'can_access_settings' ],
140 'callback' => [ $this, 'rest_system_logs_activity' ],
141 ] );
142 register_rest_route( $this->namespace, '/system/logs/activity_daily', [
143 'methods' => 'POST',
144 'permission_callback' => [ $this->core, 'can_access_settings' ],
145 'callback' => [ $this, 'rest_system_logs_activity_daily' ],
146 ] );
147 register_rest_route( $this->namespace, '/system/templates', [
148 'methods' => 'POST',
149 'permission_callback' => [ $this->core, 'can_access_features' ],
150 'callback' => [ $this, 'rest_system_templates_save' ],
151 ] );
152 register_rest_route( $this->namespace, '/system/templates', [
153 'methods' => 'GET',
154 'permission_callback' => [ $this->core, 'can_access_features' ],
155 'callback' => [ $this, 'rest_system_templates_get' ],
156 ] );
157
158 // AI Endpoints
159 register_rest_route( $this->namespace, '/ai/models', [
160 'methods' => 'POST',
161 'permission_callback' => [ $this->core, 'can_access_features' ],
162 'callback' => [ $this, 'rest_ai_models' ],
163 ] );
164 register_rest_route( $this->namespace, '/ai/test_connection', [
165 'methods' => 'POST',
166 'permission_callback' => [ $this->core, 'can_access_settings' ],
167 'callback' => [ $this, 'rest_ai_test_connection' ],
168 ] );
169 register_rest_route( $this->namespace, '/ai/completions', [
170 'methods' => 'POST',
171 'permission_callback' => [ $this->core, 'can_access_features' ],
172 'callback' => [ $this, 'rest_ai_completions' ],
173 ] );
174 register_rest_route( $this->namespace, '/ai/images', [
175 'methods' => 'POST',
176 'permission_callback' => [ $this->core, 'can_access_features' ],
177 'callback' => [ $this, 'rest_ai_images' ],
178 ] );
179 register_rest_route( $this->namespace, '/ai/image_edit', [
180 'methods' => 'POST',
181 'permission_callback' => [ $this->core, 'can_access_features' ],
182 'callback' => [ $this, 'rest_ai_image_edit' ],
183 ] );
184 register_rest_route( $this->namespace, '/ai/copilot', [
185 'methods' => 'POST',
186 'permission_callback' => [ $this->core, 'can_access_features' ],
187 'callback' => [ $this, 'rest_ai_copilot' ],
188 ] );
189
190 register_rest_route( $this->namespace, '/ai/magic_wand', [
191 'methods' => 'POST',
192 'callback' => [ $this, 'rest_ai_magic_wand' ],
193 'permission_callback' => [ $this->core, 'can_access_features' ],
194 ] );
195 register_rest_route( $this->namespace, '/ai/moderate', [
196 'methods' => 'POST',
197 'permission_callback' => [ $this->core, 'can_access_settings' ],
198 'callback' => [ $this, 'rest_ai_moderate' ],
199 ] );
200 register_rest_route( $this->namespace, '/ai/transcribe_audio', [
201 'methods' => 'POST',
202 'permission_callback' => [ $this->core, 'can_access_settings' ],
203 'callback' => [ $this, 'rest_ai_transcribe_audio' ],
204 ] );
205 register_rest_route( $this->namespace, '/ai/transcribe_image', [
206 'methods' => 'POST',
207 'permission_callback' => [ $this->core, 'can_access_settings' ],
208 'callback' => [ $this, 'rest_ai_transcribe_image' ],
209 ] );
210 register_rest_route( $this->namespace, '/ai/json', [
211 'methods' => 'POST',
212 'permission_callback' => [ $this->core, 'can_access_settings' ],
213 'callback' => [ $this, 'rest_ai_json' ],
214 ] );
215
216 // MCP Endpoints
217 register_rest_route( $this->namespace, '/mcp/functions', [
218 'methods' => 'GET',
219 'permission_callback' => [ $this->core, 'can_access_settings' ],
220 'callback' => [ $this, 'rest_mcp_functions' ],
221 ] );
222
223 // Helpers Endpoints
224 register_rest_route( $this->namespace, '/helpers/update_post_title', [
225 'methods' => 'POST',
226 'permission_callback' => [ $this->core, 'can_access_features' ],
227 'callback' => [ $this, 'rest_helpers_update_title' ],
228 ] );
229 register_rest_route( $this->namespace, '/helpers/update_post_excerpt', [
230 'methods' => 'POST',
231 'permission_callback' => [ $this->core, 'can_access_features' ],
232 'callback' => [ $this, 'rest_helpers_update_excerpt' ],
233 ] );
234 register_rest_route( $this->namespace, '/helpers/create_post', [
235 'methods' => 'POST',
236 'permission_callback' => [ $this->core, 'can_access_features' ],
237 'callback' => [ $this, 'rest_helpers_create_post' ],
238 ] );
239 register_rest_route( $this->namespace, '/helpers/create_image', [
240 'methods' => 'POST',
241 'permission_callback' => [ $this->core, 'can_access_features' ],
242 'callback' => [ $this, 'rest_helpers_create_images' ],
243 ] );
244 register_rest_route( $this->namespace, '/helpers/generate_image_meta', [
245 'methods' => 'POST',
246 'permission_callback' => [ $this->core, 'can_access_features' ],
247 'callback' => [ $this, 'rest_helpers_generate_image_meta' ],
248 ] );
249 register_rest_route( $this->namespace, '/helpers/update_media_metadata', [
250 'methods' => 'POST',
251 'permission_callback' => [ $this->core, 'can_access_features' ],
252 'callback' => [ $this, 'rest_helpers_update_media_metadata' ],
253 ] );
254 register_rest_route( $this->namespace, '/helpers/create_video', [
255 'methods' => 'POST',
256 'permission_callback' => [ $this->core, 'can_access_features' ],
257 'callback' => [ $this, 'rest_helpers_create_video' ],
258 ] );
259 register_rest_route( $this->namespace, '/helpers/video_status', [
260 'methods' => 'POST',
261 'permission_callback' => [ $this->core, 'can_access_features' ],
262 'callback' => [ $this, 'rest_helpers_video_status' ],
263 ] );
264 register_rest_route( $this->namespace, '/helpers/download_video', [
265 'methods' => 'POST',
266 'permission_callback' => [ $this->core, 'can_access_features' ],
267 'callback' => [ $this, 'rest_helpers_download_video' ],
268 ] );
269 register_rest_route( $this->namespace, '/helpers/delete_video', [
270 'methods' => 'POST',
271 'permission_callback' => [ $this->core, 'can_access_features' ],
272 'callback' => [ $this, 'rest_helpers_delete_video' ],
273 ] );
274 register_rest_route( $this->namespace, '/helpers/save_video_to_library', [
275 'methods' => 'POST',
276 'permission_callback' => [ $this->core, 'can_access_features' ],
277 'callback' => [ $this, 'rest_helpers_save_video_to_library' ],
278 ] );
279 register_rest_route( $this->namespace, '/helpers/delete_video_from_library', [
280 'methods' => 'POST',
281 'permission_callback' => [ $this->core, 'can_access_features' ],
282 'callback' => [ $this, 'rest_helpers_delete_video_from_library' ],
283 ] );
284 register_rest_route( $this->namespace, '/helpers/list_draft_media', [
285 'methods' => 'GET',
286 'permission_callback' => [ $this->core, 'can_access_features' ],
287 'callback' => [ $this, 'rest_helpers_list_draft_media' ],
288 ] );
289 register_rest_route( $this->namespace, '/helpers/approve_media', [
290 'methods' => 'POST',
291 'permission_callback' => [ $this->core, 'can_access_features' ],
292 'callback' => [ $this, 'rest_helpers_approve_media' ],
293 ] );
294 register_rest_route( $this->namespace, '/helpers/reject_media', [
295 'methods' => 'POST',
296 'permission_callback' => [ $this->core, 'can_access_features' ],
297 'callback' => [ $this, 'rest_helpers_reject_media' ],
298 ] );
299 register_rest_route( $this->namespace, '/helpers/count_posts', [
300 'methods' => 'GET',
301 'permission_callback' => [ $this->core, 'can_access_features' ],
302 'callback' => [ $this, 'rest_helpers_count_posts' ],
303 ] );
304 register_rest_route( $this->namespace, '/helpers/posts_ids', [
305 'methods' => 'GET',
306 'permission_callback' => [ $this->core, 'can_access_features' ],
307 'callback' => [ $this, 'rest_helpers_posts_ids' ],
308 ] );
309 register_rest_route( $this->namespace, '/helpers/post_types', [
310 'methods' => 'GET',
311 'permission_callback' => [ $this->core, 'can_access_features' ],
312 'callback' => [ $this, 'rest_helpers_post_types' ],
313 ] );
314 register_rest_route( $this->namespace, '/helpers/post_content', [
315 'methods' => 'GET',
316 'permission_callback' => [ $this->core, 'can_access_features' ],
317 'callback' => [ $this, 'rest_helpers_post_content' ],
318 ] );
319 register_rest_route( $this->namespace, '/helpers/check_posts_content', [
320 'methods' => 'POST',
321 'permission_callback' => [ $this->core, 'can_access_features' ],
322 'callback' => [ $this, 'rest_helpers_check_posts_content' ],
323 ] );
324 register_rest_route( $this->namespace, '/helpers/run_tasks', [
325 'methods' => 'POST',
326 'permission_callback' => [ $this->core, 'can_access_features' ],
327 'callback' => [ $this, 'rest_helpers_run_tasks' ],
328 ] );
329 register_rest_route( $this->namespace, '/helpers/optimize_database', [
330 'methods' => 'POST',
331 'permission_callback' => [ $this->core, 'can_access_settings' ],
332 'callback' => [ $this, 'rest_helpers_optimize_database' ],
333 ] );
334 register_rest_route( $this->namespace, '/helpers/cron_events', [
335 'methods' => 'GET',
336 'permission_callback' => [ $this->core, 'can_access_features' ],
337 'callback' => [ $this, 'rest_helpers_cron_events' ],
338 ] );
339 register_rest_route( $this->namespace, '/helpers/run_cron', [
340 'methods' => 'POST',
341 'permission_callback' => [ $this->core, 'can_access_features' ],
342 'callback' => [ $this, 'rest_helpers_run_cron' ],
343 ] );
344
345 // OpenAI Endpoints
346 register_rest_route( $this->namespace, '/openai/files/list', [
347 'methods' => 'GET',
348 'permission_callback' => [ $this->core, 'can_access_settings' ],
349 'callback' => [ $this, 'rest_openai_files_get' ],
350 ] );
351 register_rest_route( $this->namespace, '/openai/files/upload', [
352 'methods' => 'POST',
353 'permission_callback' => [ $this->core, 'can_access_settings' ],
354 'callback' => [ $this, 'rest_openai_files_upload' ],
355 ] );
356 register_rest_route( $this->namespace, '/openai/files/delete', [
357 'methods' => 'POST',
358 'permission_callback' => [ $this->core, 'can_access_settings' ],
359 'callback' => [ $this, 'rest_openai_files_delete' ],
360 ] );
361 register_rest_route( $this->namespace, '/openai/files/download', [
362 'methods' => 'POST',
363 'permission_callback' => [ $this->core, 'can_access_settings' ],
364 'callback' => [ $this, 'rest_openai_files_download' ],
365 ] );
366 register_rest_route( $this->namespace, '/openai/files/finetune', [
367 'methods' => 'POST',
368 'permission_callback' => [ $this->core, 'can_access_settings' ],
369 'callback' => [ $this, 'rest_openai_files_finetune' ],
370 ] );
371 register_rest_route( $this->namespace, '/openai/finetunes/list_deleted', [
372 'methods' => 'GET',
373 'permission_callback' => [ $this->core, 'can_access_settings' ],
374 'callback' => [ $this, 'rest_openai_deleted_finetunes_get' ],
375 ] );
376
377 // register_rest_route( $this->namespace, '/openai/models', array(
378 // 'methods' => 'GET',
379 // 'permission_callback' => [ $this->core, 'can_access_settings' ],
380 // 'callback' => [ $this, 'rest_openai_models_get' ],
381 // ) );
382
383 register_rest_route( $this->namespace, '/openai/finetunes/list', [
384 'methods' => 'GET',
385 'permission_callback' => [ $this->core, 'can_access_settings' ],
386 'callback' => [ $this, 'rest_openai_finetunes_get' ],
387 ] );
388 register_rest_route( $this->namespace, '/openai/finetunes/delete', [
389 'methods' => 'POST',
390 'permission_callback' => [ $this->core, 'can_access_settings' ],
391 'callback' => [ $this, 'rest_openai_finetunes_delete' ],
392 ] );
393 register_rest_route( $this->namespace, '/openai/finetunes/cancel', [
394 'methods' => 'POST',
395 'permission_callback' => [ $this->core, 'can_access_settings' ],
396 'callback' => [ $this, 'rest_openai_finetunes_cancel' ],
397 ] );
398
399 // Logging Endpoints
400 register_rest_route( $this->namespace, '/get_logs', [
401 'methods' => 'GET',
402 'permission_callback' => [ $this->core, 'can_access_features' ],
403 'callback' => [ $this, 'rest_get_logs' ]
404 ] );
405 register_rest_route( $this->namespace, '/clear_logs', [
406 'methods' => 'GET',
407 'permission_callback' => [ $this->core, 'can_access_features' ],
408 'callback' => [ $this, 'rest_clear_logs' ]
409 ] );
410
411 // Forms Endpoints
412 register_rest_route( $this->namespace, '/forms/list', [
413 'methods' => 'GET',
414 'permission_callback' => [ $this->core, 'can_access_settings' ],
415 'callback' => [ $this, 'rest_forms_list' ]
416 ] );
417 register_rest_route( $this->namespace, '/forms/get', [
418 'methods' => 'GET',
419 'permission_callback' => [ $this->core, 'can_access_settings' ],
420 'callback' => [ $this, 'rest_forms_get' ]
421 ] );
422 register_rest_route( $this->namespace, '/forms/create', [
423 'methods' => 'POST',
424 'permission_callback' => [ $this->core, 'can_access_settings' ],
425 'callback' => [ $this, 'rest_forms_create' ]
426 ] );
427 register_rest_route( $this->namespace, '/forms/update', [
428 'methods' => 'POST',
429 'permission_callback' => [ $this->core, 'can_access_settings' ],
430 'callback' => [ $this, 'rest_forms_update' ]
431 ] );
432 register_rest_route( $this->namespace, '/forms/delete', [
433 'methods' => 'POST',
434 'permission_callback' => [ $this->core, 'can_access_settings' ],
435 'callback' => [ $this, 'rest_forms_delete' ]
436 ] );
437 }
438 catch ( Exception $e ) {
439 Meow_MWAI_Logging::error( 'REST API initialization failed: ' . $e->getMessage() );
440 }
441 }
442
443 public function rest_start_session() {
444 try {
445 $sessionId = $this->core->get_session_id();
446 $restNonce = $this->core->get_nonce( true );
447
448 $response = [
449 'success' => true,
450 'sessionId' => $sessionId,
451 'restNonce' => $restNonce
452 ];
453
454 // If in test mode and we have a new token, it will be added by create_rest_response
455 // But we also want to ensure the restNonce matches the test token if available
456 if ( get_option( 'mwai_token_test_mode' ) ) {
457 $token_data = get_option( 'mwai_test_token_data' );
458 if ( $token_data && isset( $token_data['token'] ) ) {
459 $response['restNonce'] = $token_data['token'];
460 }
461 }
462
463 return $this->create_rest_response( $response, 200 );
464 }
465 catch ( Exception $e ) {
466 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
467 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
468 }
469 }
470
471 public function rest_settings_list() {
472 return $this->create_rest_response( [
473 'success' => true,
474 'options' => $this->core->get_all_options()
475 ], 200 );
476 }
477
478 public function rest_helpers_cron_events( $request ) {
479 try {
480 // Only show AI Engine cron events (those starting with mwai_)
481 $cron_events = [];
482 $crons = _get_cron_array();
483
484 // Get transient data for last run status (we'll store this when crons run)
485 $last_run_data = get_transient( 'mwai_cron_last_run' ) ?: [];
486
487 // Get all scheduled events and filter for AI Engine ones
488 foreach ( $crons as $timestamp => $cron ) {
489 foreach ( $cron as $hook => $details ) {
490 // Only process AI Engine hooks (starting with mwai_)
491 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
492 continue;
493 }
494
495 $schedule_key = array_keys( $details )[0];
496 $schedule_info = $details[$schedule_key];
497
498 // Get schedule display name
499 $schedule = $schedule_info['schedule'];
500 $schedules = wp_get_schedules();
501 $schedule_display = isset( $schedules[$schedule]['display'] ) ?
502 $schedules[$schedule]['display'] : $schedule;
503
504 $event_info = [
505 'hook' => $hook,
506 'name' => $this->get_cron_display_name( $hook ),
507 'description' => $this->get_cron_description( $hook ),
508 'next_run' => $timestamp,
509 'next_run_human' => '',
510 'last_run' => isset( $last_run_data[$hook]['time'] ) ? $last_run_data[$hook]['time'] : null,
511 'last_run_human' => isset( $last_run_data[$hook]['time'] ) ?
512 human_time_diff( $last_run_data[$hook]['time'], time() ) . ' ago' :
513 'Never',
514 'last_status' => isset( $last_run_data[$hook]['status'] ) ? $last_run_data[$hook]['status'] : 'unknown',
515 'schedule' => $schedule_display,
516 'is_running' => false,
517 'is_scheduled' => true
518 ];
519
520 // Calculate next run time properly
521 // If we have a last run time and schedule interval, calculate the actual next run
522 if ( isset( $last_run_data[$hook]['time'] ) && isset( $schedules[$schedule]['interval'] ) ) {
523 $interval = $schedules[$schedule]['interval'];
524 $last_run = $last_run_data[$hook]['time'];
525 $expected_next_run = $last_run + $interval;
526
527 // If the scheduled timestamp is in the past but we ran recently,
528 // the next run should be based on the last actual run
529 if ( $timestamp < time() &&
530 $last_run > ( time() - $interval ) ) {
531 // Cron ran recently, calculate next run from last run time
532 $event_info['next_run'] = $expected_next_run;
533 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $expected_next_run );
534 } else if ( $timestamp < time() ) {
535 // Genuinely overdue
536 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
537 } else {
538 // Future scheduled time
539 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
540 }
541 } else {
542 // No last run data, use the scheduled timestamp but be conservative about "overdue"
543 if ( $timestamp < time() ) {
544 // Only show as overdue if it's significantly past due (more than the schedule interval)
545 // to avoid false positives for crons that might be running but not tracked
546 $time_past_due = time() - $timestamp;
547 $interval = isset( $schedules[$schedule]['interval'] ) ? $schedules[$schedule]['interval'] : 3600; // Default 1 hour
548
549 if ( $time_past_due > $interval ) {
550 $event_info['next_run_human'] = 'Overdue by ' . human_time_diff( time(), $timestamp );
551 } else {
552 $event_info['next_run_human'] = 'Due to run';
553 }
554 } else {
555 $event_info['next_run_human'] = 'In ' . human_time_diff( time(), $timestamp );
556 }
557 }
558
559 // Check if currently running (via transient)
560 $running_transient = get_transient( 'mwai_cron_running_' . $hook );
561 if ( $running_transient ) {
562 $event_info['is_running'] = true;
563 }
564
565 $cron_events[] = $event_info;
566 }
567 }
568
569 return $this->create_rest_response( [ 'success' => true, 'events' => $cron_events ], 200 );
570 }
571 catch ( Exception $e ) {
572 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
573 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
574 }
575 }
576
577 public function rest_helpers_run_cron( $request ) {
578 try {
579 $params = $request->get_json_params();
580 $hook = isset( $params['hook'] ) ? $params['hook'] : null;
581
582 if ( empty( $hook ) ) {
583 return $this->create_rest_response( [ 'success' => false, 'message' => 'No cron hook provided' ], 400 );
584 }
585
586 // Only allow running AI Engine crons (starting with mwai_)
587 if ( strpos( $hook, 'mwai_' ) !== 0 ) {
588 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid cron hook' ], 400 );
589 }
590
591 // Prevent running the Tasks Runner hooks directly - they should only run via cron
592 if ( $hook === 'mwai_tasks_internal_run' || $hook === 'mwai_tasks_internal_dev_run' ) {
593 return $this->create_rest_response( [
594 'success' => false,
595 'message' => 'The Tasks Runner cannot be triggered manually. It runs automatically based on its schedule.'
596 ], 403 );
597 }
598
599 // Check if the hook exists
600 if ( !has_action( $hook ) ) {
601 return $this->create_rest_response( [ 'success' => false, 'message' => 'Cron hook not found' ], 404 );
602 }
603
604 // Run the cron action
605 do_action( $hook );
606
607 return $this->create_rest_response( [
608 'success' => true,
609 'message' => 'Cron executed successfully',
610 'hook' => $hook
611 ], 200 );
612 }
613 catch ( Exception $e ) {
614 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
615 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
616 }
617 }
618
619 private function get_cron_display_name( $hook ) {
620 $names = [
621 'mwai_tasks_internal_run' => 'Tasks Runner',
622 'mwai_tasks_internal_dev_run' => 'Tasks Runner (Dev)',
623 'mwai_cleanup_oauth' => 'OAuth Cleanup',
624 'mwai_files_cleanup' => 'Files Cleanup',
625 'mwai_discussions' => 'Discussions Cleanup'
626 ];
627 return isset( $names[$hook] ) ? $names[$hook] : $hook;
628 }
629
630 private function get_cron_description( $hook ) {
631 $descriptions = [
632 'mwai_tasks_internal_run' => 'Processes background tasks and queued operations.',
633 'mwai_tasks_internal_dev_run' => 'Processes tasks in development mode (every 5 seconds).',
634 'mwai_cleanup_oauth' => 'Cleans up expired OAuth tokens and sessions.',
635 'mwai_files_cleanup' => 'Removes expired files based on expiration dates.',
636 'mwai_discussions' => 'Maintains chat discussions database and removes old entries.'
637 ];
638 return isset( $descriptions[$hook] ) ? $descriptions[$hook] : '';
639 }
640
641 public function rest_settings_update( $request ) {
642 try {
643 $params = $request->get_json_params();
644 $value = $params['options'];
645 $options = $this->core->update_options( $value );
646 $success = !!$options;
647 $message = __( $success ? 'OK' : 'Could not update options.', 'ai-engine' );
648 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
649 }
650 catch ( Exception $e ) {
651 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
652 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
653 }
654 }
655
656 public function rest_settings_reset() {
657 try {
658 $options = $this->core->reset_options();
659 $success = !!$options;
660 $message = __( $success ? 'OK' : 'Could not reset options.', 'ai-engine' );
661 return $this->create_rest_response( [ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
662 }
663 catch ( Exception $e ) {
664 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
665 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
666 }
667 }
668
669 public function rest_ai_models( $request ) {
670 try {
671 $params = $request->get_json_params();
672 $envId = $params['envId'];
673 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
674 $models = $engine->retrieve_models();
675 return $this->create_rest_response( [ 'success' => true, 'models' => $models ], 200 );
676 }
677 catch ( Exception $e ) {
678 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
679 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
680 }
681 }
682
683 public function rest_ai_test_connection( $request ) {
684 try {
685 $params = $request->get_json_params();
686 $envId = $params['env_id'];
687
688 // Get the environment details
689 $env = null;
690 $envs = $this->core->get_option( 'ai_envs' );
691 foreach ( $envs as $e ) {
692 if ( $e['id'] === $envId ) {
693 $env = $e;
694 break;
695 }
696 }
697
698 if ( !$env ) {
699 throw new Exception( __( 'Environment not found.', 'ai-engine' ) );
700 }
701
702 // Get the engine and test connection
703 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $envId );
704 $result = $engine->connection_check();
705
706 // Format the response based on provider
707 $response = [
708 'success' => true,
709 'provider' => $env['type'],
710 'name' => $env['name'],
711 'data' => $result
712 ];
713
714 return $this->create_rest_response( $response, 200 );
715 }
716 catch ( Exception $e ) {
717 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
718 return $this->create_rest_response( [
719 'success' => false,
720 'error' => $message,
721 'provider' => isset( $env ) ? $env['type'] : 'unknown'
722 ], 200 ); // Return 200 even on error for consistent modal display
723 }
724 }
725
726 public function rest_ai_completions( $request ) {
727 try {
728 $params = $request->get_json_params();
729 $message = $this->retrieve_message( $params );
730 $query = new Meow_MWAI_Query_Text( $message );
731 $query->inject_params( $params );
732
733 // Handle streaming
734 $stream = $params['stream'] ?? false;
735 $streamCallback = null;
736 if ( $stream ) {
737 $streamCallback = function ( $reply ) use ( $query ) {
738 //$raw = _wp_specialchars( $reply, ENT_NOQUOTES, 'UTF-8', true );
739 $raw = $reply;
740 $this->core->stream_push( [ 'type' => 'live', 'data' => $raw ], $query );
741 if ( ob_get_level() > 0 ) {
742 ob_flush();
743 }
744 flush();
745 };
746 if ( headers_sent( $filename, $linenum ) ) {
747 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
748 }
749 header( 'Cache-Control: no-cache' );
750 header( 'Content-Type: text/event-stream' );
751 header( 'X-Accel-Buffering: no' ); // This is useful to disable buffering in nginx through headers.
752 ob_implicit_flush( true );
753 if ( ob_get_level() > 0 ) {
754 ob_end_flush();
755 }
756 }
757
758 // Process Reply
759 $reply = $this->core->run_query( $query, $streamCallback );
760 $restRes = [
761 'success' => true,
762 'data' => $reply->result,
763 'usage' => $reply->usage
764 ];
765 if ( $stream ) {
766 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $restRes ) ], $query );
767 die();
768 }
769 return $this->create_rest_response( $restRes, 200 );
770 }
771 catch ( Exception $e ) {
772 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
773 if ( $stream ) {
774 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
775 }
776 else {
777 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
778 }
779 }
780 }
781
782 public function rest_ai_images( $request ) {
783 try {
784 $params = $request->get_json_params();
785 $message = $this->retrieve_message( $params );
786 $query = new Meow_MWAI_Query_Image( $message );
787 $query->inject_params( $params );
788 $reply = $this->core->run_query( $query );
789 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
790 }
791 catch ( Exception $e ) {
792 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
793 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
794 }
795 }
796
797 public function rest_ai_image_edit( $request ) {
798 try {
799 // Check if this is a multipart request with files
800 $files = $request->get_file_params();
801 $params = null;
802
803 // Debug logging
804 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
805 error_log( '[AI Engine Queries] Image Edit Request - Method: ' . $request->get_method() );
806 $content_type = $request->get_content_type();
807 if ( is_array( $content_type ) ) {
808 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type['value'] );
809 }
810 else {
811 error_log( '[AI Engine Queries] Image Edit Request - Content-Type: ' . $content_type );
812 }
813 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes (' . count( $files ) . ')' : 'no' ) );
814 }
815
816 if ( !empty( $files ) ) {
817 // Handle multipart form data - get all params including POST data
818 $params = $request->get_params();
819 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
820 error_log( '[AI Engine Queries] Image Edit Request - Using form data params' );
821 }
822 }
823 else {
824 // Try to get body params first (for form data without files)
825 $body_params = $request->get_body_params();
826 if ( !empty( $body_params ) ) {
827 $params = $body_params;
828 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
829 error_log( '[AI Engine Queries] Image Edit Request - Using body params' );
830 }
831 }
832 else {
833 // Handle JSON request
834 $params = $request->get_json_params();
835 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
836 error_log( '[AI Engine Queries] Image Edit Request - Using JSON params' );
837 }
838 }
839 }
840
841 // Ensure params is always an array
842 if ( empty( $params ) ) {
843 $params = [];
844 }
845
846 // Debug logging
847 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
848 error_log( '[AI Engine Queries] Image Edit Request - Has files: ' . ( !empty( $files ) ? 'yes' : 'no' ) );
849 error_log( '[AI Engine Queries] Image Edit Request - Params: ' . json_encode( $params ) );
850 }
851
852 $message = $this->retrieve_message( $params );
853 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
854 $query = new Meow_MWAI_Query_EditImage( $message );
855
856 // The inject_params method will handle setting the file from mediaId
857 $query->inject_params( $params );
858
859 // Handle mask file if provided
860 if ( !empty( $files['mask'] ) ) {
861 $mask_file = $files['mask'];
862 if ( $mask_file['error'] === UPLOAD_ERR_OK ) {
863 $mask_data = file_get_contents( $mask_file['tmp_name'] );
864 $query->set_mask( Meow_MWAI_Query_DroppedFile::from_data( $mask_data, 'vision', $mask_file['type'] ) );
865 }
866 }
867
868 $reply = $this->core->run_query( $query );
869 return $this->create_rest_response( [ 'success' => true, 'data' => $reply->results, 'usage' => $reply->usage ], 200 );
870 }
871 catch ( Exception $e ) {
872 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
873 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
874 }
875 }
876
877 public function rest_ai_magic_wand( $request ) {
878 try {
879 $params = $request->get_json_params();
880 $action = isset( $params['action'] ) ? $params['action'] : null;
881 $data = isset( $params['data'] ) ? $params['data'] : null;
882 if ( empty( $data ) || empty( $action ) ) {
883 return $this->create_rest_response( [ 'success' => false, 'message' => 'An action and some data are required.' ], 500 );
884 }
885 $data = apply_filters( 'mwai_magic_wand_' . $action, '', $data );
886 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
887 }
888 catch ( Exception $e ) {
889 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
890 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
891 }
892 }
893
894 public function rest_ai_copilot( $request ) {
895 try {
896 $params = $request->get_json_params();
897 $action = sanitize_text_field( $params['action'] );
898 $message = $this->retrieve_message( $params, true );
899 $context = sanitize_text_field( $params['context'] );
900 $postId = !empty( $params['postId'] ) ? intval( $params['postId'] ) : null;
901 if ( empty( $action ) || empty( $message ) ) {
902 return $this->create_rest_response( [ 'success' => false, 'message' => 'Copilot needs an action and a prompt.' ], 500 );
903 }
904
905 global $mwai;
906 $result = null;
907 $params = [ 'scope' => 'copilot' ];
908
909 if ( $action === 'text' ) {
910 $prompt = "Here is the current article: \n\n===\n\n" . $context . "\n\n===\n\nIn this article, instead of the [== CURRENT BLOCK ==] placeholder, the author needs additional content. This new content should use the same tone, style, context, it should naturally flow in the article. The author shared additional information for this request:\n\n===\n\n" . $message . "\n\n===\n\nPlease provide the additional content. Only output the additional content, not the entire article, no need for extra information, and no need for the placeholders. Only output the content that should be added.";
911 if ( !empty( $model ) ) {
912 $params['model'] = $model;
913 }
914 $result = $mwai->simpleTextQuery( $prompt, $params );
915 }
916 else if ( $action === 'image' ) {
917 $prompt = "Here is the current article: \n\n===\n\n" . $context . "\n\n===\n\nIn this article, instead of the [== CURRENT BLOCK ==] placeholder, the author needs an image. Please write a detailed description (prompt) for that image that would fit this context. The image should be relevant to the article. The author shared additional information for this request:\n\n===\n\n" . $message . "\n\n===\n\nPlease only output the description for the image, not the entire article, no need for extra information, and no need for the placeholders. Only output the description.";
918
919 // Create the image
920 $simplifiedPrompt = $mwai->simpleTextQuery( $prompt, $params );
921 $media = $mwai->imageQueryForMediaLibrary( $simplifiedPrompt, $params, $postId );
922 $result = [ 'media' => $media ];
923 }
924 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
925 }
926 catch ( Exception $e ) {
927 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
928 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
929 }
930 }
931
932 public function rest_helpers_update_title( $request ) {
933 try {
934 $params = $request->get_json_params();
935 $title = sanitize_text_field( $params['title'] );
936 $postId = intval( $params['postId'] );
937 $post = get_post( $postId );
938 if ( !$post ) {
939 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
940 }
941 $post->post_title = $title;
942 //$post->post_name = sanitize_title( $title );
943 wp_update_post( $post );
944 return $this->create_rest_response( [ 'success' => true, 'message' => 'Title updated.' ], 200 );
945 }
946 catch ( Exception $e ) {
947 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
948 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
949 }
950 }
951
952 public function rest_helpers_update_excerpt( $request ) {
953 try {
954 $params = $request->get_json_params();
955 $excerpt = sanitize_text_field( $params['excerpt'] );
956 $postId = intval( $params['postId'] );
957 $post = get_post( $postId );
958 if ( !$post ) {
959 throw new Exception( __( 'There is no post with this ID.', 'ai-engine' ) );
960 }
961 $post->post_excerpt = $excerpt;
962 wp_update_post( $post );
963 return $this->create_rest_response( [ 'success' => true, 'message' => 'Excerpt updated.' ], 200 );
964 }
965 catch ( Exception $e ) {
966 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
967 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
968 }
969 }
970
971 public function rest_helpers_create_post( $request ) {
972 try {
973 $params = $request->get_json_params();
974 $title = sanitize_text_field( $params['title'] );
975 $content = sanitize_textarea_field( $params['content'] );
976 $excerpt = sanitize_text_field( $params['excerpt'] );
977 $postType = sanitize_text_field( $params['postType'] );
978 $post = new stdClass();
979 $post->post_title = $title;
980 $post->post_excerpt = $excerpt;
981 $post->post_content = $content;
982 $post->post_status = 'draft';
983 $post->post_type = isset( $postType ) ? $postType : 'post';
984 // TODO: Let's try to avoid using Markdown to create the Post
985 // Instead, we should create Gutenberg Blocks, or simple HTML.
986 // Then, we can get rid of the library for Markdown.
987 $post->post_content = $this->core->markdown_to_html( $post->post_content );
988 $postId = wp_insert_post( $post );
989 return $this->create_rest_response( [ 'success' => true, 'postId' => $postId ], 200 );
990 }
991 catch ( Exception $e ) {
992 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
993 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
994 }
995 }
996
997 public function rest_helpers_create_images( $request ) {
998 try {
999 $params = $request->get_json_params();
1000 $title = sanitize_text_field( $params['title'] );
1001 $caption = sanitize_text_field( $params['caption'] );
1002 $alt = sanitize_text_field( $params['alt'] );
1003 $description = sanitize_text_field( $params['description'] );
1004 $url = $params['url'];
1005 $filename = sanitize_text_field( $params['filename'] );
1006
1007 // Prepare AI metadata
1008 $ai_metadata = [];
1009 if ( !empty( $params['model'] ) ) {
1010 $ai_metadata['model'] = $params['model'];
1011 }
1012 if ( !empty( $params['latency'] ) ) {
1013 $ai_metadata['latency'] = $params['latency'];
1014 }
1015 if ( !empty( $params['env_id'] ) ) {
1016 $ai_metadata['env_id'] = $params['env_id'];
1017 }
1018
1019 // Debug logging
1020 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1021 error_log( '[AI Engine] create_image metadata: ' . json_encode( $ai_metadata ) );
1022 }
1023
1024 // Create as mwai_image post type (draft image)
1025 $attachmentId = $this->core->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, null, 'inherit', 'mwai_image', $ai_metadata );
1026
1027 // Add to user's draft media
1028 $user_id = get_current_user_id();
1029 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
1030 if ( !is_array( $draft_media ) ) {
1031 $draft_media = [];
1032 }
1033 $draft_media[] = [
1034 'attachment_id' => $attachmentId,
1035 'type' => 'image',
1036 'created_at' => time()
1037 ];
1038 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
1039
1040 return $this->create_rest_response( [ 'success' => true, 'attachmentId' => $attachmentId ], 200 );
1041 }
1042 catch ( Exception $e ) {
1043 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1044 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1045 }
1046 }
1047
1048 public function rest_helpers_generate_image_meta( $request ) {
1049 try {
1050 global $mwai;
1051 $params = $request->get_json_params();
1052 $attachment_id = isset( $params['attachmentId'] ) ? intval( $params['attachmentId'] ) : null;
1053
1054 if ( empty( $attachment_id ) ) {
1055 throw new Exception( __( 'The attachment ID is required.', 'ai-engine' ) );
1056 }
1057
1058 // Get the file path from the attachment ID
1059 $file_path = get_attached_file( $attachment_id );
1060 if ( empty( $file_path ) || !file_exists( $file_path ) ) {
1061 throw new Exception( __( 'Could not find the attachment file.', 'ai-engine' ) );
1062 }
1063
1064 $prompt = 'Describe this image and suggest a short title and description. '
1065 . 'Also suggest an SEO-friendly filename (lowercase, ASCII characters only, with hyphens instead of spaces). '
1066 . 'Return a JSON with the keys: title, description, filename.';
1067
1068 // Use file path instead of URL to avoid network issues
1069 $result = $mwai->simpleVisionQuery( $prompt, null, $file_path, [ 'scope' => 'admin-tools' ] );
1070 $result = preg_replace( '/^```json\s*/', '', $result );
1071 $result = preg_replace( '/\s*```$/', '', $result );
1072 if ( is_string( $result ) ) {
1073 $data = json_decode( $result, true );
1074 }
1075 else {
1076 $data = $result;
1077 }
1078 if ( !is_array( $data ) ) {
1079 $data = [];
1080 }
1081 $data = array_merge( [ 'title' => '', 'description' => '', 'filename' => '' ], $data );
1082 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1083 }
1084 catch ( Exception $e ) {
1085 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1086 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1087 }
1088 }
1089
1090 public function rest_helpers_update_media_metadata( $request ) {
1091 try {
1092 $params = $request->get_json_params();
1093 $attachment_id = intval( $params['attachmentId'] );
1094 $title = sanitize_text_field( $params['title'] ?? '' );
1095 $description = sanitize_text_field( $params['description'] ?? '' );
1096 $caption = sanitize_text_field( $params['caption'] ?? '' );
1097 $alt = sanitize_text_field( $params['alt'] ?? '' );
1098 $filename = sanitize_file_name( $params['filename'] ?? '' );
1099
1100 if ( !$attachment_id ) {
1101 throw new Exception( __( 'Attachment ID is required.', 'ai-engine' ) );
1102 }
1103
1104 // Generate slug from filename (without extension)
1105 $slug = '';
1106 if ( !empty( $filename ) ) {
1107 $slug = pathinfo( $filename, PATHINFO_FILENAME );
1108 }
1109
1110 // Update post title, content (description), caption, and slug
1111 $update_data = [
1112 'ID' => $attachment_id,
1113 'post_title' => $title,
1114 'post_content' => $description,
1115 'post_excerpt' => $caption
1116 ];
1117
1118 if ( !empty( $slug ) ) {
1119 $update_data['post_name'] = $slug;
1120 }
1121
1122 wp_update_post( $update_data );
1123
1124 // Update alt text
1125 if ( !empty( $alt ) ) {
1126 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
1127 }
1128
1129 // Update filename if provided
1130 $new_url = null;
1131 if ( !empty( $filename ) ) {
1132 $file_path = get_attached_file( $attachment_id );
1133 if ( $file_path ) {
1134 $path_parts = pathinfo( $file_path );
1135 $new_file_path = $path_parts['dirname'] . '/' . $filename;
1136 if ( rename( $file_path, $new_file_path ) ) {
1137 update_attached_file( $attachment_id, $new_file_path );
1138 // Build new URL from file path for custom post types
1139 $upload_dir = wp_upload_dir();
1140 $new_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $new_file_path );
1141 }
1142 }
1143 }
1144
1145 $response = [ 'success' => true ];
1146 if ( $new_url ) {
1147 $response['url'] = $new_url;
1148 }
1149
1150 return $this->create_rest_response( $response, 200 );
1151 }
1152 catch ( Exception $e ) {
1153 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1154 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1155 }
1156 }
1157
1158 public function rest_openai_files_get() {
1159 try {
1160 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1161 $purposeFilter = isset( $_GET['purpose'] ) ? $_GET['purpose'] : null;
1162 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1163 $files = $openai->list_files( $purposeFilter );
1164 return $this->create_rest_response( [ 'success' => true, 'files' => $files ], 200 );
1165 }
1166 catch ( Exception $e ) {
1167 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1168 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1169 }
1170 }
1171
1172 public function rest_openai_deleted_finetunes_get() {
1173 try {
1174 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1175 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1176 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1177 $finetunes = $openai->list_deleted_finetunes( $legacy );
1178 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1179 }
1180 catch ( Exception $e ) {
1181 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1182 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1183 }
1184 }
1185
1186 public function rest_openai_finetunes_get() {
1187 try {
1188 $envId = isset( $_GET['envId'] ) ? $_GET['envId'] : null;
1189 $legacy = isset( $_GET['legacy'] ) ? $_GET['legacy'] === 'true' : false;
1190 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1191 $finetunes = $openai->list_finetunes( $legacy );
1192 return $this->create_rest_response( [ 'success' => true, 'finetunes' => $finetunes ], 200 );
1193 }
1194 catch ( Exception $e ) {
1195 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1196 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1197 }
1198 }
1199
1200 public function rest_openai_files_upload( $request ) {
1201 try {
1202 $params = $request->get_json_params();
1203 $envId = $params['envId'];
1204 ;
1205 $filename = sanitize_text_field( $params['filename'] );
1206 $data = $params['data'];
1207 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1208 $file = $openai->upload_file( $filename, $data );
1209 return $this->create_rest_response( [ 'success' => true, 'file' => $file ], 200 );
1210 }
1211 catch ( Exception $e ) {
1212 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1213 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1214 }
1215 }
1216
1217 public function rest_openai_files_delete( $request ) {
1218 try {
1219 $params = $request->get_json_params();
1220 $envId = $params['envId'];
1221 ;
1222 $fileId = $params['fileId'];
1223 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1224 $openai->delete_file( $fileId );
1225 return $this->create_rest_response( [ 'success' => true ], 200 );
1226 }
1227 catch ( Exception $e ) {
1228 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1229 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1230 }
1231 }
1232
1233 public function rest_openai_finetunes_cancel( $request ) {
1234 try {
1235 $params = $request->get_json_params();
1236 $envId = $params['envId'];
1237 ;
1238 $finetuneId = $params['finetuneId'];
1239 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1240 $openai->cancel_finetune( $finetuneId );
1241 return $this->create_rest_response( [ 'success' => true ], 200 );
1242 }
1243 catch ( Exception $e ) {
1244 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1245 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1246 }
1247 }
1248
1249 public function rest_openai_finetunes_delete( $request ) {
1250 try {
1251 $params = $request->get_json_params();
1252 $envId = $params['envId'];
1253 ;
1254 $modelId = $params['modelId'];
1255 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1256 $openai->delete_finetune( $modelId );
1257 return $this->create_rest_response( [ 'success' => true ], 200 );
1258 }
1259 catch ( Exception $e ) {
1260 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1261 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1262 }
1263 }
1264
1265 public function rest_openai_files_download( $request ) {
1266 try {
1267 $params = $request->get_json_params();
1268 $envId = $params['envId'];
1269 ;
1270 $fileId = $params['fileId'];
1271 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1272 $filename = $openai->download_file( $fileId );
1273 $data = file_get_contents( $filename );
1274 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1275 }
1276 catch ( Exception $e ) {
1277 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1278 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1279 }
1280 }
1281
1282 public function rest_openai_files_finetune( $request ) {
1283 try {
1284 $params = $request->get_json_params();
1285 $envId = $params['envId'];
1286 ;
1287 $fileId = $params['fileId'];
1288 $model = $params['model'];
1289 $suffix = $params['suffix'];
1290 $hyperparams = [
1291 'nEpochs' => isset( $params['nEpochs'] ) ? $params['nEpochs'] : null,
1292 'batchSize' => isset( $params['batchSize'] ) ? $params['batchSize'] : null,
1293 ];
1294 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1295 $finetune = $openai->run_finetune( $fileId, $model, $suffix, $hyperparams );
1296 return $this->create_rest_response( [ 'success' => true, 'finetune' => $finetune ], 200 );
1297 }
1298 catch ( Exception $e ) {
1299 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1300 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1301 }
1302 }
1303
1304 public function rest_helpers_count_posts( $request ) {
1305 try {
1306 $params = $request->get_query_params();
1307 $postType = $params['postType'];
1308 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1309 $count = wp_count_posts( $postType );
1310 $count = array_sum( array_intersect_key( (array) $count, array_flip( $postStatus ) ) );
1311 return $this->create_rest_response( [ 'success' => true, 'count' => $count ], 200 );
1312 }
1313 catch ( Exception $e ) {
1314 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1315 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1316 }
1317 }
1318
1319 public function rest_helpers_posts_ids( $request ) {
1320 try {
1321 global $wpdb;
1322 $params = $request->get_query_params();
1323 $postType = $params['postType'];
1324 $postStatus = !empty( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1325
1326 // Use direct SQL query instead of get_posts to avoid memory issues with large sites
1327 $statusPlaceholders = implode( ',', array_fill( 0, count( $postStatus ), '%s' ) );
1328 $query = "SELECT ID FROM {$wpdb->posts}
1329 WHERE post_type = %s
1330 AND post_status IN ($statusPlaceholders)
1331 ORDER BY ID ASC";
1332
1333 $prepareArgs = array_merge( [ $postType ], $postStatus );
1334 $postIds = $wpdb->get_col( $wpdb->prepare( $query, ...$prepareArgs ) );
1335 $postIds = array_map( 'intval', $postIds );
1336
1337 return $this->create_rest_response( [ 'success' => true, 'postIds' => $postIds ], 200 );
1338 }
1339 catch ( Exception $e ) {
1340 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1341 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1342 }
1343 }
1344
1345 public function rest_helpers_post_content( $request ) {
1346 try {
1347 $params = $request->get_query_params();
1348 $offset = (int) $params['offset'];
1349 $postType = $params['postType'];
1350 $postStatus = isset( $params['postStatus'] ) ? explode( ',', $params['postStatus'] ) : [ 'publish' ];
1351 $postId = (int) $params['postId'];
1352
1353 $post = null;
1354 if ( !empty( $postId ) ) {
1355 $post = get_post( $postId );
1356 if ( $post->post_status !== 'publish' && $post->post_status !== 'future'
1357 && $post->post_status !== 'draft' && $post->post_status !== 'private' ) {
1358 $post = null;
1359 }
1360 }
1361 else {
1362 $posts = get_posts( [
1363 'posts_per_page' => 1,
1364 'post_type' => $postType,
1365 'offset' => $offset,
1366 'post_status' => $postStatus,
1367 ] );
1368 $post = count( $posts ) === 0 ? null : $posts[0];
1369 }
1370 if ( !$post ) {
1371 return $this->create_rest_response( [ 'success' => false, 'message' => 'Post not found' ], 404 );
1372 }
1373 $cleanPost = $this->core->get_post( $post );
1374 return $this->create_rest_response( [ 'success' => true, 'content' => $cleanPost['content'],
1375 'checksum' => $cleanPost['checksum'], 'language' => $cleanPost['language'], 'excerpt' => $cleanPost['excerpt'],
1376 'postId' => $cleanPost['postId'], 'title' => $cleanPost['title'], 'url' => $cleanPost['url'] ], 200 );
1377 }
1378 catch ( Exception $e ) {
1379 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1380 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1381 }
1382 }
1383
1384 // Batch check which posts have content (for Push All optimization)
1385 public function rest_helpers_check_posts_content( $request ) {
1386 try {
1387 global $wpdb;
1388 $params = $request->get_json_params();
1389 $postIds = isset( $params['postIds'] ) ? $params['postIds'] : [];
1390
1391 if ( empty( $postIds ) || !is_array( $postIds ) ) {
1392 return $this->create_rest_response( [
1393 'success' => false,
1394 'message' => 'postIds array is required'
1395 ], 400 );
1396 }
1397
1398 // Sanitize post IDs
1399 $postIds = array_map( 'intval', $postIds );
1400
1401 // Use single SQL query to check for non-empty content
1402 // Note: This checks raw post_content only, not content added via shortcodes/blocks
1403 // For most embeddings use cases, checking saved content is sufficient
1404
1405 // Chunk IDs to avoid max_allowed_packet issues with very large sites (100k+ posts)
1406 $chunkSize = 5000;
1407 $postsWithContent = [];
1408
1409 foreach ( array_chunk( $postIds, $chunkSize ) as $chunk ) {
1410 $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
1411 $query = "SELECT ID FROM {$wpdb->posts}
1412 WHERE ID IN ($placeholders)
1413 AND post_content != ''
1414 AND post_content IS NOT NULL";
1415
1416 $chunkResults = $wpdb->get_col( $wpdb->prepare( $query, ...$chunk ) );
1417 $postsWithContent = array_merge( $postsWithContent, array_map( 'intval', $chunkResults ) );
1418 }
1419
1420 return $this->create_rest_response( [
1421 'success' => true,
1422 'postsWithContent' => $postsWithContent
1423 ], 200 );
1424 }
1425 catch ( Exception $e ) {
1426 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1427 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1428 }
1429 }
1430
1431 public function rest_helpers_run_tasks( $request ) {
1432 try {
1433 // Prevent concurrent execution with a transient lock
1434 $lock_key = 'mwai_rest_run_tasks_lock';
1435 if ( get_transient( $lock_key ) ) {
1436 // Log excessive calls for debugging
1437 if ( $this->core->get_option( 'dev_mode' ) ) {
1438 error_log( '[AI Engine] WARNING: rest_helpers_run_tasks called while already running' );
1439 }
1440 return $this->create_rest_response( [
1441 'success' => false,
1442 'message' => 'Tasks are already running. Please wait.'
1443 ], 429 ); // 429 Too Many Requests
1444 }
1445
1446 // Set lock for 30 seconds
1447 set_transient( $lock_key, true, 30 );
1448
1449 // Log task execution start
1450 if ( $this->core->get_option( 'dev_mode' ) ) {
1451 error_log( '[AI Engine] rest_helpers_run_tasks triggered via REST API' );
1452 }
1453
1454 try {
1455 do_action( 'mwai_tasks_run' );
1456 delete_transient( $lock_key );
1457 return $this->create_rest_response( [ 'success' => true ], 200 );
1458 }
1459 catch ( Exception $e ) {
1460 delete_transient( $lock_key );
1461 throw $e;
1462 }
1463 }
1464 catch ( Exception $e ) {
1465 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1466 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1467 }
1468 }
1469
1470 public function rest_helpers_optimize_database( $request ) {
1471 try {
1472 global $wpdb;
1473 $results = [];
1474
1475 // Add indexes to optimize query performance
1476 $indexes = [
1477 // mwai_logs indexes
1478 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time', 'columns' => 'time' ],
1479 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_userId', 'columns' => 'userId' ],
1480 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_envId', 'columns' => 'envId' ],
1481 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_refId', 'columns' => 'refId' ],
1482 [ 'table' => 'mwai_logs', 'name' => 'idx_mwai_logs_time_model', 'columns' => 'time, model' ],
1483
1484 // mwai_logmeta indexes
1485 [ 'table' => 'mwai_logmeta', 'name' => 'idx_mwai_logmeta_log_id', 'columns' => 'log_id' ],
1486
1487 // mwai_vectors indexes
1488 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_envId_status_dbId', 'columns' => 'envId, status, dbId' ],
1489 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_refId', 'columns' => 'refId' ],
1490 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_status', 'columns' => 'status' ],
1491 [ 'table' => 'mwai_vectors', 'name' => 'idx_mwai_vectors_updated', 'columns' => 'updated' ],
1492
1493 // mwai_files indexes
1494 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_expires', 'columns' => 'expires' ],
1495 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_userId', 'columns' => 'userId' ],
1496 [ 'table' => 'mwai_files', 'name' => 'idx_mwai_files_purpose', 'columns' => 'purpose' ],
1497
1498 // mwai_filemeta indexes
1499 [ 'table' => 'mwai_filemeta', 'name' => 'idx_mwai_filemeta_file_id', 'columns' => 'file_id' ],
1500
1501 // mwai_chats indexes
1502 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_botId', 'columns' => 'chatId, botId' ],
1503 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_chatId_userId', 'columns' => 'chatId, userId' ],
1504 [ 'table' => 'mwai_chats', 'name' => 'idx_mwai_chats_updated', 'columns' => 'updated' ],
1505 ];
1506
1507 // Add indexes
1508 foreach ( $indexes as $index ) {
1509 $table = $wpdb->prefix . $index['table'];
1510 $index_name = $index['name'];
1511 $columns = $index['columns'];
1512
1513 // Check if index already exists
1514 $existing = $wpdb->get_var( $wpdb->prepare(
1515 "SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
1516 WHERE table_schema = %s AND table_name = %s AND index_name = %s",
1517 DB_NAME, $table, $index_name
1518 ) );
1519
1520 if ( !$existing ) {
1521 $wpdb->query( "ALTER TABLE `$table` ADD INDEX `$index_name` ($columns)" );
1522 $results[] = "Added index $index_name on $table";
1523 }
1524 }
1525
1526 // Clean up old logs (older than 3 months)
1527 $three_months_ago = date( 'Y-m-d H:i:s', strtotime( '-3 months' ) );
1528
1529 // Delete old logs
1530 $deleted_logs = $wpdb->query( $wpdb->prepare(
1531 "DELETE FROM {$wpdb->prefix}mwai_logs WHERE time < %s",
1532 $three_months_ago
1533 ) );
1534 $results[] = "Deleted $deleted_logs old log entries";
1535
1536 // Delete orphaned logmeta
1537 $deleted_logmeta = $wpdb->query(
1538 "DELETE lm FROM {$wpdb->prefix}mwai_logmeta lm
1539 LEFT JOIN {$wpdb->prefix}mwai_logs l ON lm.log_id = l.id
1540 WHERE l.id IS NULL"
1541 );
1542 $results[] = "Deleted $deleted_logmeta orphaned logmeta entries";
1543
1544 // Delete old chats (older than 3 months)
1545 $deleted_chats = $wpdb->query( $wpdb->prepare(
1546 "DELETE FROM {$wpdb->prefix}mwai_chats WHERE updated < %s",
1547 $three_months_ago
1548 ) );
1549 $results[] = "Deleted $deleted_chats old chat discussions";
1550
1551 // Optimize tables
1552 $tables = [ 'mwai_logs', 'mwai_logmeta', 'mwai_vectors', 'mwai_files', 'mwai_filemeta', 'mwai_chats' ];
1553 foreach ( $tables as $table ) {
1554 $wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}$table" );
1555 }
1556 $results[] = "Optimized all AI Engine tables";
1557
1558 $message = implode( "\n", $results );
1559 return $this->create_rest_response( [ 'success' => true, 'message' => $message ], 200 );
1560 }
1561 catch ( Exception $e ) {
1562 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1563 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1564 }
1565 }
1566
1567 public function rest_system_templates_get( $request ) {
1568 try {
1569 $params = $request->get_query_params();
1570 $category = $params['category'];
1571 $templates = [];
1572 $templates_option = get_option( 'mwai_templates', [] );
1573 if ( !is_array( $templates_option ) ) {
1574 update_option( 'mwai_templates', [] );
1575 }
1576 $categories = array_column( $templates_option, 'category' );
1577 $index = array_search( $category, $categories );
1578 $templates = [];
1579 if ( $index !== false ) {
1580 $templates = $templates_option[$index]['templates'];
1581 }
1582 return $this->create_rest_response( [ 'success' => true, 'templates' => $templates ], 200 );
1583 }
1584 catch ( Exception $e ) {
1585 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1586 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1587 }
1588 }
1589
1590 public function rest_system_templates_save( $request ) {
1591 try {
1592 $params = $request->get_json_params();
1593 $category = $params['category'];
1594 $templates = $params['templates'];
1595 $templates_option = get_option( 'mwai_templates', [] );
1596 $categories = array_column( $templates_option, 'category' );
1597 $index = array_search( $category, $categories );
1598 if ( $index !== false && $index >= 0 ) {
1599 $templates_option[$index]['templates'] = $templates;
1600 }
1601 else {
1602 $group = [ 'category' => $category, 'templates' => $templates ];
1603 $templates_option[] = $group;
1604 }
1605
1606 update_option( 'mwai_templates', $templates_option );
1607 return $this->create_rest_response( [ 'success' => true ], 200 );
1608 }
1609 catch ( Exception $e ) {
1610 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1611 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1612 }
1613 }
1614
1615 public function rest_system_logs_list( $request ) {
1616 try {
1617 $params = $request->get_json_params();
1618 $offset = $params['offset'];
1619 $limit = $params['limit'];
1620 $filters = $params['filters'];
1621 $sort = isset( $params['sort'] ) ? $params['sort'] : null;
1622 $logs = apply_filters( 'mwai_stats_logs_list', [], $offset, $limit, $filters, $sort );
1623 return $this->create_rest_response( [ 'success' => true, 'total' => $logs['total'], 'logs' => $logs['rows'] ], 200 );
1624 }
1625 catch ( Exception $e ) {
1626 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1627 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1628 }
1629 }
1630
1631 public function rest_system_logs_delete( $request ) {
1632 try {
1633 $params = $request->get_json_params();
1634 $logIds = $params['logIds'];
1635 $success = apply_filters( 'mwai_stats_logs_delete', true, $logIds );
1636 return $this->create_rest_response( [ 'success' => $success ], 200 );
1637 }
1638 catch ( Exception $e ) {
1639 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1640 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1641 }
1642 }
1643
1644 public function rest_system_logs_meta_get( $request ) {
1645 try {
1646 $params = $request->get_json_params();
1647 $logId = $params['logId'];
1648 $metaKeys = $params['metaKeys'];
1649 $data = apply_filters( 'mwai_stats_logs_meta', [], $logId, $metaKeys );
1650 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1651 }
1652 catch ( Exception $e ) {
1653 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1654 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1655 }
1656 }
1657
1658 public function rest_system_logs_activity( $request ) {
1659 try {
1660 $params = $request->get_json_params();
1661 $hours = isset( $params['hours'] ) ? intval( $params['hours'] ) : 24;
1662 $data = apply_filters( 'mwai_stats_logs_activity', [], $hours );
1663 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1664 }
1665 catch ( Exception $e ) {
1666 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1667 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1668 }
1669 }
1670
1671 public function rest_system_logs_activity_daily( $request ) {
1672 try {
1673 $params = $request->get_json_params();
1674 $days = isset( $params['days'] ) ? intval( $params['days'] ) : 31;
1675 $byModel = isset( $params['byModel'] ) ? (bool) $params['byModel'] : false;
1676
1677 if ( $byModel ) {
1678 $data = apply_filters( 'mwai_stats_logs_activity_daily_by_model', [], $days );
1679 } else {
1680 $data = apply_filters( 'mwai_stats_logs_activity_daily', [], $days );
1681 }
1682
1683 return $this->create_rest_response( [ 'success' => true, 'data' => $data ], 200 );
1684 }
1685 catch ( Exception $e ) {
1686 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1687 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1688 }
1689 }
1690
1691 public function rest_ai_moderate( $request ) {
1692 try {
1693 $params = $request->get_json_params();
1694 $envId = $params['envId'];
1695 $text = $params['text'];
1696 if ( !$text ) {
1697 return $this->create_rest_response( [ 'success' => false, 'message' => 'Text not found.' ], 404 );
1698 }
1699 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $envId );
1700 $results = $openai->moderate( $text );
1701 return $this->create_rest_response( [ 'success' => true, 'results' => $results ], 200 );
1702 }
1703 catch ( Exception $e ) {
1704 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1705 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1706 }
1707 }
1708
1709 public function rest_ai_transcribe_audio( $request ) {
1710 try {
1711 global $mwai;
1712 $params = $request->get_json_params();
1713 $url = !empty( $params['url'] ) ? $params['url'] : null;
1714 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
1715 $path = !empty( $params['path'] ) ? $params['path'] : null;
1716
1717 // If mediaId is provided, get the file path
1718 if ( !$path && $mediaId > 0 ) {
1719 $path = get_attached_file( $mediaId );
1720 if ( empty( $path ) ) {
1721 throw new Exception( __( 'The media file cannot be found.', 'ai-engine' ) );
1722 }
1723 }
1724
1725 // Set the scope for admin tools
1726 if ( !isset( $params['scope'] ) ) {
1727 $params['scope'] = 'admin-tools';
1728 }
1729
1730 $result = $mwai->simpleTranscribeAudio( $url, $path, $params );
1731 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1732 }
1733 catch ( Exception $e ) {
1734 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1735 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1736 }
1737 }
1738
1739 public function rest_ai_transcribe_image( $request ) {
1740 try {
1741 global $mwai;
1742 $params = $request->get_json_params();
1743 $message = $this->retrieve_message( $params );
1744 $url = !empty( $params['url'] ) ? $params['url'] : null;
1745 // This could lead to a security issue, so let's avoid using path directly.
1746 //$path = !empty( $params['path'] ) ? $params['path'] : null;
1747 $result = $mwai->simpleVisionQuery( $message, $url );
1748 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1749 }
1750 catch ( Exception $e ) {
1751 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1752 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1753 }
1754 }
1755
1756 public function rest_ai_json( $request ) {
1757 try {
1758 global $mwai;
1759 $params = $request->get_json_params();
1760 $message = $this->retrieve_message( $params );
1761 $result = $mwai->simpleJsonQuery( $message );
1762 return $this->create_rest_response( [ 'success' => true, 'data' => $result ], 200 );
1763 }
1764 catch ( Exception $e ) {
1765 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1766 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1767 }
1768 }
1769
1770 public function rest_mcp_functions( $request ) {
1771 try {
1772 // Get all registered MCP tools
1773 $tools = apply_filters( 'mwai_mcp_tools', [] );
1774
1775 // Format the response
1776 $response = [
1777 'success' => true,
1778 'count' => count( $tools ),
1779 'functions' => $tools
1780 ];
1781
1782 return $this->create_rest_response( $response, 200 );
1783 }
1784 catch ( Exception $e ) {
1785 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1786 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1787 }
1788 }
1789
1790 public function rest_helpers_post_types() {
1791 try {
1792 $postTypes = $this->core->get_post_types();
1793 return $this->create_rest_response( [ 'success' => true, 'postTypes' => $postTypes ], 200 );
1794 }
1795 catch ( Exception $e ) {
1796 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1797 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1798 }
1799 }
1800
1801 public function rest_settings_themes( $request ) {
1802 try {
1803 $method = $request->get_method();
1804 if ( $method === 'GET' ) {
1805 $themes = $this->core->get_themes();
1806 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
1807 }
1808 else if ( $method === 'POST' ) {
1809 $params = $request->get_json_params();
1810 $themes = $params['themes'];
1811 $themes = $this->core->update_themes( $themes );
1812 return $this->create_rest_response( [ 'success' => true, 'themes' => $themes ], 200 );
1813 }
1814 }
1815 catch ( Exception $e ) {
1816 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1817 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1818 }
1819 }
1820
1821 public function rest_settings_chatbots( $request ) {
1822 try {
1823 $method = $request->get_method();
1824 if ( $method === 'GET' ) {
1825 $chatbots = $this->core->get_chatbots();
1826 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
1827 }
1828 else if ( $method === 'POST' ) {
1829 $params = $request->get_json_params();
1830 $chatbots = $params['chatbots'];
1831 $chatbots = $this->core->update_chatbots( $chatbots );
1832 return $this->create_rest_response( [ 'success' => true, 'chatbots' => $chatbots ], 200 );
1833 }
1834 return $this->create_rest_response( [ 'success' => false, 'message' => 'Method not allowed' ], 405 );
1835 }
1836 catch ( Exception $e ) {
1837 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
1838 return $this->create_rest_response( [ 'success' => false, 'message' => $message ], 500 );
1839 }
1840 }
1841
1842 #region Logs
1843
1844 public function rest_get_logs() {
1845 $logs = Meow_MWAI_Logging::get();
1846 return $this->create_rest_response( [ 'success' => true, 'data' => $logs ], 200 );
1847 }
1848
1849 public function rest_clear_logs() {
1850 Meow_MWAI_Logging::clear();
1851 return $this->create_rest_response( [ 'success' => true ], 200 );
1852 }
1853
1854 #endregion
1855
1856 #region Forms
1857
1858 public function rest_forms_list( $request ) {
1859 try {
1860 $args = [
1861 'post_type' => 'mwai_form',
1862 'posts_per_page' => 100,
1863 'post_status' => 'any',
1864 'orderby' => 'date',
1865 'order' => 'DESC'
1866 ];
1867
1868 $posts = get_posts( $args );
1869 $forms = array_map( function( $post ) {
1870 return [
1871 'id' => $post->ID,
1872 'title' => $post->post_title,
1873 'status' => $post->post_status
1874 ];
1875 }, $posts );
1876
1877 return $this->create_rest_response( [ 'success' => true, 'forms' => $forms ], 200 );
1878 }
1879 catch ( Exception $e ) {
1880 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1881 }
1882 }
1883
1884 public function rest_forms_get( $request ) {
1885 try {
1886 $id = intval( $request->get_param( 'id' ) );
1887 if ( !$id ) {
1888 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
1889 }
1890
1891 $post = get_post( $id );
1892 if ( !$post || $post->post_type !== 'mwai_form' ) {
1893 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
1894 }
1895
1896 $form = [
1897 'id' => $post->ID,
1898 'title' => [
1899 'raw' => $post->post_title,
1900 'rendered' => $post->post_title
1901 ],
1902 'content' => [
1903 'raw' => $post->post_content,
1904 'rendered' => $post->post_content
1905 ],
1906 'status' => $post->post_status
1907 ];
1908
1909 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
1910 }
1911 catch ( Exception $e ) {
1912 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1913 }
1914 }
1915
1916 public function rest_forms_create( $request ) {
1917 try {
1918 $params = $request->get_json_params();
1919 $title = isset( $params['title'] ) ? $params['title'] : 'Untitled Form';
1920
1921 $post_data = [
1922 'post_title' => $title,
1923 'post_content' => '',
1924 'post_status' => 'draft',
1925 'post_type' => 'mwai_form'
1926 ];
1927
1928 $post_id = wp_insert_post( $post_data );
1929
1930 if ( is_wp_error( $post_id ) ) {
1931 return $this->create_rest_response( [ 'success' => false, 'message' => $post_id->get_error_message() ], 500 );
1932 }
1933
1934 $post = get_post( $post_id );
1935 $form = [
1936 'id' => $post->ID,
1937 'title' => [
1938 'raw' => $post->post_title,
1939 'rendered' => $post->post_title
1940 ],
1941 'status' => $post->post_status
1942 ];
1943
1944 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
1945 }
1946 catch ( Exception $e ) {
1947 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
1948 }
1949 }
1950
1951 public function rest_forms_update( $request ) {
1952 try {
1953 $params = $request->get_json_params();
1954 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
1955
1956 if ( !$id ) {
1957 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
1958 }
1959
1960 $post = get_post( $id );
1961 if ( !$post || $post->post_type !== 'mwai_form' ) {
1962 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
1963 }
1964
1965 $post_data = [ 'ID' => $id ];
1966
1967 if ( isset( $params['title'] ) ) {
1968 $post_data['post_title'] = $params['title'];
1969 }
1970
1971 if ( isset( $params['content'] ) ) {
1972 $post_data['post_content'] = $params['content'];
1973 }
1974
1975 if ( isset( $params['status'] ) ) {
1976 $post_data['post_status'] = $params['status'];
1977 }
1978
1979 $result = wp_update_post( $post_data );
1980
1981 if ( is_wp_error( $result ) ) {
1982 return $this->create_rest_response( [ 'success' => false, 'message' => $result->get_error_message() ], 500 );
1983 }
1984
1985 $post = get_post( $id );
1986 $form = [
1987 'id' => $post->ID,
1988 'title' => [
1989 'raw' => $post->post_title,
1990 'rendered' => $post->post_title
1991 ],
1992 'content' => [
1993 'raw' => $post->post_content,
1994 'rendered' => $post->post_content
1995 ],
1996 'status' => $post->post_status
1997 ];
1998
1999 return $this->create_rest_response( [ 'success' => true, 'form' => $form ], 200 );
2000 }
2001 catch ( Exception $e ) {
2002 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2003 }
2004 }
2005
2006 public function rest_forms_delete( $request ) {
2007 try {
2008 $params = $request->get_json_params();
2009 $id = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
2010
2011 if ( !$id ) {
2012 return $this->create_rest_response( [ 'success' => false, 'message' => 'Invalid form ID' ], 400 );
2013 }
2014
2015 $post = get_post( $id );
2016 if ( !$post || $post->post_type !== 'mwai_form' ) {
2017 return $this->create_rest_response( [ 'success' => false, 'message' => 'Form not found' ], 404 );
2018 }
2019
2020 $result = wp_delete_post( $id, true );
2021
2022 if ( !$result ) {
2023 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to delete form' ], 500 );
2024 }
2025
2026 return $this->create_rest_response( [ 'success' => true ], 200 );
2027 }
2028 catch ( Exception $e ) {
2029 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2030 }
2031 }
2032
2033 #endregion
2034
2035 #region Video Generation Helpers
2036
2037 public function rest_helpers_create_video( $request ) {
2038 try {
2039 $params = $request->get_json_params();
2040 $prompt = sanitize_text_field( $params['prompt'] );
2041 $model = sanitize_text_field( $params['model'] ?? 'sora-2' );
2042 $size = sanitize_text_field( $params['size'] ?? '720x1280' );
2043 $seconds = absint( $params['seconds'] ?? 4 );
2044 $envId = sanitize_text_field( $params['envId'] ?? '' );
2045
2046 // Check if envId is provided (defaults not supported for videos yet)
2047 if ( empty( $envId ) ) {
2048 throw new Exception( 'Please select a specific environment and model in the Video Generator. Default environments are not yet supported for video generation.' );
2049 }
2050
2051 // Get API key from environment
2052 $env = $this->core->get_ai_env( $envId );
2053 $api_key = $env['apikey'] ?? '';
2054
2055 if ( empty( $api_key ) ) {
2056 throw new Exception( 'OpenAI API key not found.' );
2057 }
2058
2059 // Prepare multipart boundary
2060 $boundary = wp_generate_password( 24, false );
2061 $body = '';
2062
2063 // Add model
2064 $body .= "--{$boundary}\r\n";
2065 $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
2066 $body .= "{$model}\r\n";
2067
2068 // Add prompt
2069 $body .= "--{$boundary}\r\n";
2070 $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
2071 $body .= "{$prompt}\r\n";
2072
2073 // Add size
2074 $body .= "--{$boundary}\r\n";
2075 $body .= "Content-Disposition: form-data; name=\"size\"\r\n\r\n";
2076 $body .= "{$size}\r\n";
2077
2078 // Add seconds
2079 $body .= "--{$boundary}\r\n";
2080 $body .= "Content-Disposition: form-data; name=\"seconds\"\r\n\r\n";
2081 $body .= "{$seconds}\r\n";
2082
2083 $body .= "--{$boundary}--\r\n";
2084
2085 // Call OpenAI API to create video
2086 $response = wp_remote_post( 'https://api.openai.com/v1/videos', [
2087 'headers' => [
2088 'Authorization' => 'Bearer ' . $api_key,
2089 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
2090 ],
2091 'body' => $body,
2092 'timeout' => 30
2093 ] );
2094
2095 if ( is_wp_error( $response ) ) {
2096 throw new Exception( $response->get_error_message() );
2097 }
2098
2099 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
2100
2101 if ( isset( $response_body['error'] ) ) {
2102 throw new Exception( $response_body['error']['message'] ?? 'Unknown error' );
2103 }
2104
2105 // Record usage (price is calculated per second)
2106 $usage = $this->core->record_videos_usage( $model, $size, $seconds );
2107
2108 // Log to Query Logs (Statistics)
2109 try {
2110 if ( class_exists( 'MeowPro_MWAI_Stats' ) && class_exists( 'MeowPro_MWAI_Statistics' ) ) {
2111 $statsObject = new MeowPro_MWAI_Stats();
2112 $statsObject->session = $params['session'] ?? null;
2113 $statsObject->scope = 'admin-tools';
2114 $statsObject->feature = 'video-generator';
2115 $statsObject->model = $model;
2116 $statsObject->envId = $envId;
2117 $statsObject->units = $seconds;
2118 $statsObject->type = 'seconds';
2119 $statsObject->price = $usage['price'] ?? 0;
2120 $statsObject->accuracy = $usage['accuracy'] ?? 'full';
2121
2122 $statistics = new MeowPro_MWAI_Statistics();
2123 $statistics->commit_stats( $statsObject );
2124 }
2125 }
2126 catch ( Exception $statsError ) {
2127 // Log the error but don't fail the video creation
2128 error_log( '[AI Engine Video] Failed to log statistics: ' . $statsError->getMessage() );
2129 }
2130
2131 // Store metadata for later retrieval when video completes
2132 if ( isset( $response_body['id'] ) ) {
2133 set_transient( 'mwai_video_metadata_' . $response_body['id'], [
2134 'model' => $model,
2135 'env_id' => $envId,
2136 'created_at' => time()
2137 ], 7 * DAY_IN_SECONDS );
2138 }
2139
2140 return $this->create_rest_response( [ 'success' => true, 'video' => $response_body, 'usage' => $usage ], 200 );
2141 }
2142 catch ( Exception $e ) {
2143 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2144 }
2145 }
2146
2147 public function rest_helpers_video_status( $request ) {
2148 try {
2149 $params = $request->get_json_params();
2150 $video_ids = $params['videoIds'] ?? [];
2151 $envId = sanitize_text_field( $params['envId'] ?? '' );
2152
2153 if ( empty( $video_ids ) ) {
2154 return $this->create_rest_response( [ 'success' => true, 'videos' => [] ], 200 );
2155 }
2156
2157 // Get API key from environment
2158 $env = $this->core->get_ai_env( $envId );
2159 $api_key = $env['apikey'] ?? '';
2160
2161 if ( empty( $api_key ) ) {
2162 throw new Exception( 'OpenAI API key not found.' );
2163 }
2164
2165 $videos = [];
2166 foreach ( $video_ids as $video_id ) {
2167 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id, [
2168 'headers' => [
2169 'Authorization' => 'Bearer ' . $api_key
2170 ],
2171 'timeout' => 15
2172 ] );
2173
2174 if ( !is_wp_error( $response ) ) {
2175 $body = json_decode( wp_remote_retrieve_body( $response ), true );
2176 if ( !isset( $body['error'] ) ) {
2177 // If video is completed and we haven't saved it yet, download and save to media library
2178 if ( $body['status'] === 'completed' && empty( get_transient( 'mwai_video_saved_' . $video_id ) ) ) {
2179 // Retrieve metadata that was stored when video was created
2180 $metadata = get_transient( 'mwai_video_metadata_' . $video_id );
2181 $ai_metadata = [];
2182 if ( $metadata ) {
2183 $ai_metadata = [
2184 'model' => $metadata['model'] ?? null,
2185 'env_id' => $metadata['env_id'] ?? null,
2186 'latency' => isset( $metadata['created_at'] ) ? ( time() - $metadata['created_at'] ) : null
2187 ];
2188 }
2189
2190 $attachment_id = $this->download_and_save_video( $video_id, $api_key, '', '', $ai_metadata );
2191 if ( $attachment_id ) {
2192 $body['attachment_id'] = $attachment_id;
2193 // Build URL from file path for custom post types
2194 $file_path = get_attached_file( $attachment_id );
2195 $upload_dir = wp_upload_dir();
2196 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2197 // Mark as saved so we don't download again
2198 set_transient( 'mwai_video_saved_' . $video_id, $attachment_id, DAY_IN_SECONDS );
2199 // Clean up metadata transient
2200 delete_transient( 'mwai_video_metadata_' . $video_id );
2201 }
2202 }
2203 // Check if we already have this video saved
2204 else if ( $body['status'] === 'completed' ) {
2205 $attachment_id = get_transient( 'mwai_video_saved_' . $video_id );
2206 if ( $attachment_id ) {
2207 $body['attachment_id'] = $attachment_id;
2208 // Build URL from file path for custom post types
2209 $file_path = get_attached_file( $attachment_id );
2210 $upload_dir = wp_upload_dir();
2211 $body['url'] = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2212 }
2213 }
2214 $videos[] = $body;
2215 }
2216 else {
2217 // Include error information in the response
2218 error_log( 'AI Engine: Video generation failed for ID ' . $video_id . ': ' . json_encode( $body['error'] ) );
2219 $videos[] = [
2220 'id' => $video_id,
2221 'status' => 'failed',
2222 'error' => $body['error']
2223 ];
2224 }
2225 }
2226 else {
2227 // WP HTTP error
2228 error_log( 'AI Engine: Failed to check video status for ID ' . $video_id . ': ' . $response->get_error_message() );
2229 $videos[] = [
2230 'id' => $video_id,
2231 'status' => 'failed',
2232 'error' => [ 'message' => $response->get_error_message() ]
2233 ];
2234 }
2235 }
2236
2237 return $this->create_rest_response( [ 'success' => true, 'videos' => $videos ], 200 );
2238 }
2239 catch ( Exception $e ) {
2240 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2241 }
2242 }
2243
2244 public function rest_helpers_download_video( $request ) {
2245 try {
2246 $params = $request->get_json_params();
2247 $video_id = sanitize_text_field( $params['videoId'] );
2248 $envId = sanitize_text_field( $params['envId'] ?? '' );
2249
2250 // Get API key from environment
2251 $env = $this->core->get_ai_env( $envId );
2252 $api_key = $env['apikey'] ?? '';
2253
2254 if ( empty( $api_key ) ) {
2255 throw new Exception( 'OpenAI API key not found.' );
2256 }
2257
2258 $temp_file = wp_tempnam( $video_id . '.mp4' );
2259
2260 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2261 'headers' => [
2262 'Authorization' => 'Bearer ' . $api_key
2263 ],
2264 'timeout' => 120,
2265 'stream' => true,
2266 'filename' => $temp_file
2267 ] );
2268
2269 if ( is_wp_error( $response ) ) {
2270 throw new Exception( $response->get_error_message() );
2271 }
2272
2273 $file_data = file_get_contents( $temp_file );
2274 $base64 = base64_encode( $file_data );
2275
2276 unlink( $temp_file );
2277
2278 return $this->create_rest_response( [
2279 'success' => true,
2280 'data' => $base64,
2281 'mimeType' => 'video/mp4'
2282 ], 200 );
2283 }
2284 catch ( Exception $e ) {
2285 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2286 }
2287 }
2288
2289 public function rest_helpers_delete_video( $request ) {
2290 try {
2291 $params = $request->get_json_params();
2292 $video_id = sanitize_text_field( $params['videoId'] );
2293 $envId = sanitize_text_field( $params['envId'] ?? '' );
2294
2295 // Get API key from environment
2296 $env = $this->core->get_ai_env( $envId );
2297 $api_key = $env['apikey'] ?? '';
2298
2299 if ( empty( $api_key ) ) {
2300 throw new Exception( 'OpenAI API key not found.' );
2301 }
2302
2303 $response = wp_remote_request( 'https://api.openai.com/v1/videos/' . $video_id, [
2304 'method' => 'DELETE',
2305 'headers' => [
2306 'Authorization' => 'Bearer ' . $api_key
2307 ],
2308 'timeout' => 15
2309 ] );
2310
2311 if ( is_wp_error( $response ) ) {
2312 throw new Exception( $response->get_error_message() );
2313 }
2314
2315 return $this->create_rest_response( [ 'success' => true ], 200 );
2316 }
2317 catch ( Exception $e ) {
2318 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2319 }
2320 }
2321
2322 private function download_and_save_video( $video_id, $api_key, $title = '', $description = '', $ai_metadata = [] ) {
2323 try {
2324 // Download video content
2325 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2326 'headers' => [
2327 'Authorization' => 'Bearer ' . $api_key
2328 ],
2329 'timeout' => 120
2330 ] );
2331
2332 if ( is_wp_error( $response ) ) {
2333 error_log( 'Error downloading video: ' . $response->get_error_message() );
2334 return false;
2335 }
2336
2337 $video_data = wp_remote_retrieve_body( $response );
2338 if ( empty( $video_data ) ) {
2339 error_log( 'Empty video data received' );
2340 return false;
2341 }
2342
2343 // Generate filename
2344 $filename = $video_id . '.mp4';
2345 $upload_dir = wp_upload_dir();
2346 $file_path = $upload_dir['path'] . '/' . $filename;
2347
2348 // Save to file
2349 file_put_contents( $file_path, $video_data );
2350
2351 // Prepare attachment data - use mwai_video post type (draft video)
2352 $attachment = [
2353 'post_mime_type' => 'video/mp4',
2354 'post_title' => !empty( $title ) ? $title : 'AI Generated Video',
2355 'post_content' => $description,
2356 'post_status' => 'inherit',
2357 'post_type' => 'mwai_video'
2358 ];
2359
2360 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
2361 $attachment_id = wp_insert_post( $attachment );
2362
2363 // Set the attached file manually since we're not using wp_insert_attachment
2364 update_attached_file( $attachment_id, $file_path );
2365
2366 if ( is_wp_error( $attachment_id ) ) {
2367 error_log( 'Error creating attachment: ' . $attachment_id->get_error_message() );
2368 return false;
2369 }
2370
2371 // Generate attachment metadata
2372 require_once ABSPATH . 'wp-admin/includes/image.php';
2373 $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
2374 wp_update_attachment_metadata( $attachment_id, $attach_data );
2375
2376 // Store AI-related metadata
2377 if ( !empty( $ai_metadata['model'] ) ) {
2378 update_post_meta( $attachment_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
2379 }
2380 if ( !empty( $ai_metadata['latency'] ) ) {
2381 update_post_meta( $attachment_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
2382 }
2383 if ( !empty( $ai_metadata['env_id'] ) ) {
2384 update_post_meta( $attachment_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
2385 }
2386
2387 // Add to user's draft media
2388 $user_id = get_current_user_id();
2389 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2390 if ( !is_array( $draft_media ) ) {
2391 $draft_media = [];
2392 }
2393 $draft_media[] = [
2394 'attachment_id' => $attachment_id,
2395 'type' => 'video',
2396 'openai_id' => $video_id,
2397 'created_at' => time()
2398 ];
2399 update_user_meta( $user_id, 'mwai_draft_media', $draft_media );
2400
2401 return $attachment_id;
2402 }
2403 catch ( Exception $e ) {
2404 error_log( 'Exception in download_and_save_video: ' . $e->getMessage() );
2405 return false;
2406 }
2407 }
2408
2409 public function rest_helpers_save_video_to_library( $request ) {
2410 try {
2411 $params = $request->get_json_params();
2412 $video_id = sanitize_text_field( $params['videoId'] );
2413 $title = sanitize_text_field( $params['title'] );
2414 $description = sanitize_text_field( $params['description'] );
2415 $filename = sanitize_file_name( $params['filename'] );
2416 $envId = sanitize_text_field( $params['envId'] ?? '' );
2417
2418 // Ensure filename has .mp4 extension
2419 if ( !preg_match( '/\.mp4$/i', $filename ) ) {
2420 $filename .= '.mp4';
2421 }
2422
2423 // Get API key from environment
2424 $env = $this->core->get_ai_env( $envId );
2425 $api_key = $env['apikey'] ?? '';
2426
2427 if ( empty( $api_key ) ) {
2428 throw new Exception( 'OpenAI API key not found.' );
2429 }
2430
2431 // Download video content
2432 $response = wp_remote_get( 'https://api.openai.com/v1/videos/' . $video_id . '/content', [
2433 'headers' => [
2434 'Authorization' => 'Bearer ' . $api_key
2435 ],
2436 'timeout' => 120
2437 ] );
2438
2439 if ( is_wp_error( $response ) ) {
2440 throw new Exception( $response->get_error_message() );
2441 }
2442
2443 $video_data = wp_remote_retrieve_body( $response );
2444
2445 // Upload to WordPress media library
2446 $upload_dir = wp_upload_dir();
2447 $file_path = $upload_dir['path'] . '/' . $filename;
2448
2449 file_put_contents( $file_path, $video_data );
2450
2451 $attachment = [
2452 'post_mime_type' => 'video/mp4',
2453 'post_title' => $title,
2454 'post_content' => $description,
2455 'post_status' => 'inherit'
2456 ];
2457
2458 $attach_id = wp_insert_attachment( $attachment, $file_path );
2459
2460 require_once( ABSPATH . 'wp-admin/includes/image.php' );
2461 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
2462 wp_update_attachment_metadata( $attach_id, $attach_data );
2463
2464 return $this->create_rest_response( [
2465 'success' => true,
2466 'attachmentId' => $attach_id
2467 ], 200 );
2468 }
2469 catch ( Exception $e ) {
2470 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2471 }
2472 }
2473
2474 public function rest_helpers_delete_video_from_library( $request ) {
2475 try {
2476 $params = $request->get_json_params();
2477 $attachment_id = absint( $params['attachmentId'] );
2478
2479 if ( empty( $attachment_id ) ) {
2480 throw new Exception( 'Attachment ID is required.' );
2481 }
2482
2483 $deleted = wp_delete_attachment( $attachment_id, true );
2484
2485 if ( !$deleted ) {
2486 throw new Exception( 'Failed to delete attachment.' );
2487 }
2488
2489 return $this->create_rest_response( [ 'success' => true ], 200 );
2490 }
2491 catch ( Exception $e ) {
2492 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2493 }
2494 }
2495
2496 public function rest_helpers_list_draft_media( $request ) {
2497 try {
2498 $type = $request->get_param( 'type' ); // 'image', 'video', or null for all
2499 $user_id = get_current_user_id();
2500 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2501
2502 if ( !is_array( $draft_media ) ) {
2503 return $this->create_rest_response( [ 'success' => true, 'media' => [] ], 200 );
2504 }
2505
2506 $media_items = [];
2507 foreach ( $draft_media as $item ) {
2508 // Filter by type if specified
2509 if ( $type && $item['type'] !== $type ) {
2510 continue;
2511 }
2512
2513 $attachment_id = $item['attachment_id'];
2514 $attachment = get_post( $attachment_id );
2515
2516 if ( $attachment ) {
2517 // For custom post types (mwai_image, mwai_video), build URL from file path
2518 $file_path = get_attached_file( $attachment_id );
2519 $upload_dir = wp_upload_dir();
2520 $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file_path );
2521
2522 $model = get_post_meta( $attachment_id, 'mwai_model', true );
2523 $generation_time = get_post_meta( $attachment_id, 'mwai_latency', true );
2524 $env_id = get_post_meta( $attachment_id, 'mwai_env_id', true );
2525
2526 // Debug logging
2527 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2528 error_log( '[AI Engine] list_draft_media - attachment_id: ' . $attachment_id . ' model: ' . var_export( $model, true ) . ' generation_time: ' . var_export( $generation_time, true ) . ' env_id: ' . var_export( $env_id, true ) );
2529 }
2530
2531 $media_items[] = [
2532 'attachment_id' => $attachment_id,
2533 'type' => $item['type'],
2534 'openai_id' => $item['openai_id'] ?? null,
2535 'url' => $url,
2536 'title' => $attachment->post_title,
2537 'description' => $attachment->post_content,
2538 'filename' => basename( $file_path ),
2539 'created_at' => $item['created_at'],
2540 'model' => $model,
2541 'generation_time' => $generation_time,
2542 'env_id' => $env_id
2543 ];
2544 }
2545 }
2546
2547 return $this->create_rest_response( [ 'success' => true, 'media' => $media_items ], 200 );
2548 }
2549 catch ( Exception $e ) {
2550 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2551 }
2552 }
2553
2554 public function rest_helpers_approve_media( $request ) {
2555 try {
2556 $params = $request->get_json_params();
2557 $attachment_id = absint( $params['attachmentId'] );
2558 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2559 $envId = sanitize_text_field( $params['envId'] ?? '' );
2560
2561 if ( empty( $attachment_id ) ) {
2562 throw new Exception( 'Attachment ID is required.' );
2563 }
2564
2565 // Convert from mwai_image/mwai_video to attachment post type
2566 wp_update_post( [
2567 'ID' => $attachment_id,
2568 'post_type' => 'attachment',
2569 'post_status' => 'inherit'
2570 ] );
2571
2572 // Remove from draft media list
2573 $user_id = get_current_user_id();
2574 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2575 if ( is_array( $draft_media ) ) {
2576 $draft_media = array_filter( $draft_media, function( $item ) use ( $attachment_id ) {
2577 return $item['attachment_id'] !== $attachment_id;
2578 } );
2579 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2580 }
2581
2582 // Delete video from OpenAI if applicable
2583 if ( !empty( $openai_id ) ) {
2584 $env = $this->core->get_ai_env( $envId );
2585 $api_key = $env['apikey'] ?? '';
2586
2587 if ( !empty( $api_key ) ) {
2588 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2589 'method' => 'DELETE',
2590 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2591 'timeout' => 15
2592 ] );
2593 }
2594 }
2595
2596 return $this->create_rest_response( [ 'success' => true ], 200 );
2597 }
2598 catch ( Exception $e ) {
2599 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2600 }
2601 }
2602
2603 public function rest_helpers_reject_media( $request ) {
2604 try {
2605 $params = $request->get_json_params();
2606 $attachment_id = absint( $params['attachmentId'] );
2607 $openai_id = sanitize_text_field( $params['openaiId'] ?? '' );
2608 $envId = sanitize_text_field( $params['envId'] ?? '' );
2609
2610 if ( empty( $attachment_id ) ) {
2611 throw new Exception( 'Attachment ID is required.' );
2612 }
2613
2614 // Convert from mwai_image/mwai_video to attachment post type first
2615 // This ensures wp_delete_attachment properly deletes the physical file
2616 wp_update_post( [
2617 'ID' => $attachment_id,
2618 'post_type' => 'attachment'
2619 ] );
2620
2621 // Delete attachment from WordPress (now that it's a proper attachment, files will be deleted)
2622 wp_delete_attachment( $attachment_id, true );
2623
2624 // Remove from draft media list
2625 $user_id = get_current_user_id();
2626 $draft_media = get_user_meta( $user_id, 'mwai_draft_media', true );
2627 if ( is_array( $draft_media ) ) {
2628 $draft_media = array_filter( $draft_media, function( $item ) use ( $attachment_id ) {
2629 return $item['attachment_id'] !== $attachment_id;
2630 } );
2631 update_user_meta( $user_id, 'mwai_draft_media', array_values( $draft_media ) );
2632 }
2633
2634 // Delete video from OpenAI if applicable
2635 if ( !empty( $openai_id ) ) {
2636 $env = $this->core->get_ai_env( $envId );
2637 $api_key = $env['apikey'] ?? '';
2638
2639 if ( !empty( $api_key ) ) {
2640 wp_remote_request( 'https://api.openai.com/v1/videos/' . $openai_id, [
2641 'method' => 'DELETE',
2642 'headers' => [ 'Authorization' => 'Bearer ' . $api_key ],
2643 'timeout' => 15
2644 ] );
2645 }
2646 }
2647
2648 return $this->create_rest_response( [ 'success' => true ], 200 );
2649 }
2650 catch ( Exception $e ) {
2651 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
2652 }
2653 }
2654
2655 #endregion
2656 }
2657