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