PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.3
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 / modules / discussions.php
ai-engine / classes / modules Last commit date
advisor.php 3 months ago chatbot.php 1 month ago discussions.php 2 months ago editor-assistant.php 3 months ago files.php 3 months ago forms-manager.php 3 months ago gdpr.php 4 months ago search.php 3 months ago security.php 11 months ago tasks-examples.php 6 months ago tasks.php 1 month ago wand.php 3 months ago
discussions.php
885 lines
1 <?php
2
3 class Meow_MWAI_Modules_Discussions {
4 private $wpdb = null;
5 private $core = null;
6 public $table_chats = null;
7 private $db_check = false;
8 private $namespace_admin = 'mwai/v1';
9 private $namespace_ui = 'mwai-ui/v1';
10
11 public function __construct() {
12 global $wpdb;
13 $this->wpdb = $wpdb;
14 global $mwai_core;
15 $this->core = $mwai_core;
16 $this->table_chats = $wpdb->prefix . 'mwai_chats';
17
18 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
19 add_filter( 'mwai_chatbot_reply', [ $this, 'chatbot_reply' ], 10, 4 );
20 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
21
22 // Register task handler
23 add_filter( 'mwai_task_cleanup_discussions', [ $this, 'handle_cleanup_task' ], 10, 2 );
24 }
25 }
26
27 public function rest_api_init() {
28 // Admin
29 register_rest_route( $this->namespace_admin, '/discussions/list', [
30 'methods' => 'POST',
31 'callback' => [ $this, 'rest_discussions_list' ],
32 'permission_callback' => [ $this->core, 'can_access_settings' ],
33 ] );
34 register_rest_route( $this->namespace_admin, '/discussions/delete', [
35 'methods' => 'POST',
36 'callback' => [ $this, 'rest_discussions_delete_admin' ],
37 'permission_callback' => [ $this->core, 'can_access_settings' ],
38 ] );
39
40 // UI
41 register_rest_route( $this->namespace_ui, '/discussions/list', [
42 'methods' => 'POST',
43 'callback' => [ $this, 'rest_discussions_ui_list' ],
44 'permission_callback' => '__return_true'
45 ] );
46 register_rest_route( $this->namespace_ui, '/discussions/edit', [
47 'methods' => 'POST',
48 'callback' => [ $this, 'rest_discussions_ui_edit' ],
49 'permission_callback' => '__return_true'
50 ] );
51 register_rest_route( $this->namespace_ui, '/discussions/delete', [
52 'methods' => 'POST',
53 'callback' => [ $this, 'rest_discussions_delete' ],
54 'permission_callback' => [ $this, 'can_delete_discussion' ],
55 ] );
56 }
57
58 public function can_delete_discussion( $request ) {
59 $this->check_db();
60 $params = $request->get_json_params();
61 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
62 $userId = get_current_user_id();
63 if ( !$userId ) {
64 return false;
65 }
66 foreach ( $chatIds as $chatId ) {
67 $chat = $this->wpdb->get_row(
68 $this->wpdb->prepare(
69 "SELECT * FROM $this->table_chats WHERE chatId = %s",
70 $chatId
71 )
72 );
73 if ( !$chat || (int) $chat->userId !== (int) $userId ) {
74 return false;
75 }
76 }
77 return true;
78 }
79
80 /**
81 * Helper method to create REST responses with automatic token refresh
82 *
83 * @param array $data The response data
84 * @param int $status HTTP status code
85 * @return WP_REST_Response
86 */
87 protected function create_rest_response( $data, $status = 200 ) {
88 // Always check if we need to provide a new nonce
89 $current_nonce = $this->core->get_nonce( true );
90 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
91
92 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
93 // We'll refresh if the nonce is older than 10 hours to be safe
94 $should_refresh = false;
95
96 if ( $request_nonce ) {
97 // Try to determine the age of the nonce
98 // WordPress uses a tick system where each tick is 12 hours
99 // If we're in the second half of the nonce's life, refresh it
100 $time = time();
101 $nonce_tick = wp_nonce_tick();
102
103 // Verify if the nonce is still valid but getting old
104 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
105 if ( $verify === 2 ) {
106 // Nonce is valid but was generated 12-24 hours ago
107 $should_refresh = true;
108 // Log will be written when token is included in response
109 }
110 }
111
112 // If the nonce has changed or should be refreshed, include the new one
113 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
114 $data['new_token'] = $current_nonce;
115
116 // Log if server debug mode is enabled
117 if ( $this->core->get_option( 'server_debug_mode' ) ) {
118 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
119 }
120 }
121
122 return new WP_REST_Response( $data, $status );
123 }
124
125 /**
126 * Generate or update the title for a specific discussion
127 * by calling the AI (if it meets the requirements).
128 *
129 * @param stdClass $discussion A row from the DB (object form).
130 * @return void
131 */
132 private function generate_title_for_discussion( $discussion ) {
133 // Check if there's already a title
134 if ( !empty( $discussion->title ) ) {
135 return; // Nothing to do if title is already set.
136 }
137
138 // Ensure it's not older than 10 days, or whatever logic you prefer
139 $ten_days_ago = strtotime( '-10 days' );
140 if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
141 return; // Skip if older than 10 days
142 }
143
144 // We expect JSON in the messages
145 $messages = json_decode( $discussion->messages, true );
146 if ( !is_array( $messages ) ) {
147 return;
148 }
149
150 // Check for at least one user and one assistant message
151 $has_user_message = false;
152 $has_assistant_message = false;
153 foreach ( $messages as $message ) {
154 if ( isset( $message['role'] ) ) {
155 if ( $message['role'] === 'user' ) {
156 $has_user_message = true;
157 }
158 if ( $message['role'] === 'assistant' ) {
159 $has_assistant_message = true;
160 }
161 }
162 if ( $has_user_message && $has_assistant_message ) {
163 break;
164 }
165 }
166
167 if ( !( $has_user_message && $has_assistant_message ) ) {
168 return; // If doesn't have both, skip
169 }
170
171 // Prepare the conversation text for the prompt
172 $conversation_text = '';
173 foreach ( $messages as $message ) {
174 if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
175 $role = ucfirst( $message['role'] );
176 $content = $message['content'];
177 $conversation_text .= "$role: $content\n";
178 }
179 }
180
181 $base_prompt = __( 'Based on the following conversation, generate a concise and specific title for the discussion, strictly less than 64 characters. Focus on the main topic, avoiding unnecessary words such as articles, pronouns, or adjectives. Do not include any punctuation at the end. Do not include anything else than the title itself, only one sentence, no line breaks, just the title.', 'ai-engine' ) . "\n\n" . __( 'Conversation:', 'ai-engine' ) . "\n$conversation_text\n";
182 $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );
183
184 // Run the AI query using the fast environment
185 global $mwai;
186 $params = [ 'scope' => 'discussions' ];
187
188 // Use simpleFastTextQuery which handles Fast Model configuration
189 try {
190 $answer = $mwai->simpleFastTextQuery( $prompt, $params );
191
192 // Clean up the answer
193 $title = trim( $answer );
194 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
195 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
196 if ( empty( $title ) ) {
197 $title = __( 'Untitled', 'ai-engine' );
198 }
199 }
200 catch ( Exception $e ) {
201 // Handle content filter or other API errors
202 $error_message = $e->getMessage();
203 if ( strpos( $error_message, 'content_filter' ) !== false ||
204 strpos( $error_message, 'ResponsibleAIPolicyViolation' ) !== false ) {
205 error_log( "AI Engine: Content filter blocked title generation for discussion ID {$discussion->id}. Using fallback title." );
206 $title = __( 'Discussion', 'ai-engine' ) . ' ' . date( 'Y-m-d H:i' );
207 }
208 else {
209 error_log( "AI Engine: Failed to generate title for discussion ID {$discussion->id}: " . $error_message );
210 $title = __( 'Untitled', 'ai-engine' );
211 }
212 }
213
214 // Update the discussion with the title
215 $updated = $this->wpdb->update(
216 $this->table_chats,
217 [ 'title' => $title ],
218 [ 'id' => $discussion->id ]
219 );
220 if ( $updated === false ) {
221 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
222 }
223 }
224
225 /**
226 * Admin route for listing discussions. No forced logic here.
227 */
228 public function rest_discussions_list( $request ) {
229 try {
230 $params = $request->get_json_params();
231 $offset = $params['offset'];
232 $limit = $params['limit'];
233 $filters = $params['filters'];
234 $sort = $params['sort'];
235
236 // Retrieve the chats
237 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
238
239 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
240 }
241 catch ( Exception $e ) {
242 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
243 }
244 }
245
246 public function rest_discussions_ui_edit( $request ) {
247 try {
248 $params = $request->get_json_params();
249 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
250 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
251
252 if ( is_null( $chatId ) || is_null( $title ) ) {
253 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
254 }
255
256 $userId = get_current_user_id();
257 if ( !$userId ) {
258 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
259 }
260
261 // Update the discussion title for the current user
262 $updated = $this->wpdb->update(
263 $this->table_chats,
264 [ 'title' => $title ],
265 [ 'chatId' => $chatId, 'userId' => $userId ]
266 );
267 if ( $updated === false ) {
268 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
269 }
270
271 return $this->create_rest_response( [ 'success' => true ], 200 );
272 }
273 catch ( Exception $e ) {
274 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
275 }
276 }
277
278 public function cron_discussions() {
279 // Track cron execution start
280 $this->core->track_cron_start( 'mwai_discussions' );
281
282 try {
283 $this->check_db();
284
285 // NEW CHECK: Only run if auto-titling is enabled
286 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
287 $this->core->track_cron_end( 'mwai_discussions', 'success' );
288 return;
289 }
290 // END NEW CHECK
291
292 // Set the current user to the first admin to avoid guest limits
293 $admin_users = get_users( [ 'role' => 'administrator', 'number' => 1 ] );
294 if ( !empty( $admin_users ) ) {
295 $admin_user = $admin_users[0];
296 wp_set_current_user( $admin_user->ID );
297 }
298
299 $now = date( 'Y-m-d H:i:s' );
300 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
301
302 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
303 $query = $this->wpdb->prepare(
304 "SELECT * FROM {$this->table_chats}
305 WHERE title IS NULL AND updated >= %s
306 ORDER BY updated DESC LIMIT 5",
307 $ten_days_ago
308 );
309 $discussions = $this->wpdb->get_results( $query );
310 if ( empty( $discussions ) ) {
311 $this->core->track_cron_end( 'mwai_discussions', 'success' );
312 return;
313 }
314
315 foreach ( $discussions as $discussion ) {
316 $this->generate_title_for_discussion( $discussion );
317 }
318
319 $this->core->track_cron_end( 'mwai_discussions', 'success' );
320 }
321 catch ( Exception $e ) {
322 $this->core->track_cron_end( 'mwai_discussions', 'error', $e->getMessage() );
323 }
324 }
325
326 /**
327 * UI route for listing discussions.
328 * Here we add the "forced cron" logic for up to 5 discussions,
329 * but only if auto-titling is enabled.
330 */
331 public function rest_discussions_ui_list( $request ) {
332 try {
333 $params = $request->get_json_params();
334 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
335 // Get paging setting from options
336 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
337 if ( $paging_option === 'None' ) {
338 $default_limit = 999; // Show all discussions
339 }
340 else {
341 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
342 }
343 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
344 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
345 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
346
347 if ( !is_null( $customId ) ) {
348 $botId = $customId;
349 }
350 if ( is_null( $botId ) ) {
351 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
352 }
353
354 $userId = get_current_user_id();
355 if ( !$userId ) {
356 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
357 }
358
359 $filters = [
360 [ 'accessor' => 'user', 'value' => $userId ],
361 [ 'accessor' => 'botId', 'value' => $botId ],
362 ];
363
364 // Retrieve the chats
365 $chats = $this->chats_query( [], $offset, $limit, $filters );
366
367 // NEW CHECK: only do forced titling if it's enabled
368 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
369 // "Forced cron" logic: check up to 5 that have no title
370 $counter = 0;
371 foreach ( $chats['rows'] as &$chatRow ) {
372 if ( $counter >= 5 ) {
373 break;
374 }
375 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
376 $discussionObj = (object) $chatRow;
377 $this->generate_title_for_discussion( $discussionObj );
378 $counter++;
379 }
380 }
381 // If you want the newly-updated titles to show up *immediately*:
382 $chats = $this->chats_query( [], $offset, $limit, $filters );
383 }
384 // END NEW CHECK
385
386 // Apply filters to discussion metadata
387 foreach ( $chats['rows'] as &$chatRow ) {
388 // Decode messages JSON to get the count
389 $messages = json_decode( $chatRow['messages'], true );
390 $message_count = is_array( $messages ) ? count( $messages ) : 0;
391
392 // Add formatted metadata that can be filtered
393 $chatRow['metadata_display'] = [
394 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
395 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
396 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
397 ];
398 }
399
400 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
401 }
402 catch ( Exception $e ) {
403 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
404 }
405 }
406
407 public function rest_discussions_delete_admin( $request ) {
408 try {
409 $this->check_db();
410 $params = $request->get_json_params();
411 $chatsIds = $params['chatIds'];
412 if ( is_array( $chatsIds ) ) {
413 if ( count( $chatsIds ) === 0 ) {
414 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
415 }
416 foreach ( $chatsIds as $chatId ) {
417 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
418 }
419 }
420 return $this->create_rest_response( [ 'success' => true ], 200 );
421 }
422 catch ( Exception $e ) {
423 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
424 }
425 }
426
427 public function rest_discussions_delete( $request ) {
428 try {
429 $this->check_db();
430 $params = $request->get_json_params();
431 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
432
433 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
434 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
435 }
436
437 $userId = get_current_user_id();
438 if ( !$userId ) {
439 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
440 }
441
442 foreach ( $chatIds as $chatId ) {
443 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
444 }
445
446 return $this->create_rest_response( [ 'success' => true ], 200 );
447 }
448 catch ( Exception $e ) {
449 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
450 }
451 }
452
453 // Get latest discussion for the given parameter
454 public function get_discussion( $botId, $chatId ) {
455 $this->check_db();
456 $chat = $this->wpdb->get_row(
457 $this->wpdb->prepare(
458 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
459 $chatId,
460 $botId
461 ),
462 ARRAY_A
463 );
464 if ( $chat ) {
465 $chat['messages'] = json_decode( $chat['messages'] );
466 return $chat;
467 }
468 return null;
469 }
470
471 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
472 $this->check_db();
473 $offset = !empty( $offset ) ? intval( $offset ) : 0;
474 $limit = !empty( $limit ) ? intval( $limit ) : 5;
475 $filters = !empty( $filters ) ? $filters : [];
476 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
477
478 $where_clauses = [];
479 $where_values = [];
480
481 if ( is_array( $filters ) ) {
482 foreach ( $filters as $filter ) {
483 $value = $filter['value'];
484 if ( is_null( $value ) || $value === '' ) {
485 continue;
486 }
487 switch ( $filter['accessor'] ) {
488 case 'user':
489 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
490 if ( $isIP ) {
491 $where_clauses[] = 'ip = %s';
492 $where_values[] = $value;
493 }
494 else {
495 $where_clauses[] = 'userId = %d';
496 $where_values[] = intval( $value );
497 }
498 break;
499 case 'botId':
500 $where_clauses[] = 'botId = %s';
501 $where_values[] = $value;
502 break;
503 case 'preview':
504 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
505 $where_clauses[] = 'messages LIKE %s';
506 $where_values[] = $like;
507 break;
508 // Add other cases as needed
509 }
510 }
511 }
512
513 $where_sql = '';
514 if ( !empty( $where_clauses ) ) {
515 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
516 }
517 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
518
519 $limit_sql = '';
520 if ( $limit > 0 ) {
521 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
522 }
523
524 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
525 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
526
527 // Get the total count
528 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
529 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
530
531 return $chats;
532 }
533
534 public function chatbot_reply( $rawText, $reply, $params, $extra ) {
535 global $mwai_core;
536 $query = $reply->query;
537 $userIp = $mwai_core->get_ip_address();
538 $userId = $mwai_core->get_user_id();
539 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
540 $chatId = $this->core->fix_chat_id( $query, $params );
541 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
542 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
543 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
544 $now = date( 'Y-m-d H:i:s' );
545
546 if ( !empty( $customId ) ) {
547 $botId = $customId;
548 }
549 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
550 $shortcutName = isset( $params['shortcutName'] ) ? $params['shortcutName'] : null;
551
552 // If there are images, add them to the message for display purposes
553 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
554 foreach ( $attachments as $attachedFile ) {
555 if ( $attachedFile->is_image() && $attachedFile->get_type() === 'url' ) {
556 $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
557 }
558 }
559
560 $this->check_db();
561 $chat = $this->wpdb->get_row(
562 $this->wpdb->prepare(
563 "SELECT * FROM $this->table_chats WHERE chatId = %s",
564 $chatId
565 )
566 );
567 $messageExtra = [
568 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
569 ];
570 $chatExtra = [
571 'session' => $query->session,
572 'model' => $query->model,
573 ];
574 if ( !empty( $query->temperature ) ) {
575 $chatExtra['temperature'] = $query->temperature;
576 }
577 if ( !empty( $query->context ) ) {
578 $chatExtra['context'] = $query->context;
579 }
580 if ( !empty( $params['parentBotId'] ) ) {
581 $chatExtra['parentBotId'] = $params['parentBotId'];
582 }
583 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
584 $chatExtra['assistantId'] = $query->assistantId;
585 $chatExtra['threadId'] = $query->threadId;
586 $chatExtra['storeId'] = $query->storeId;
587 }
588
589 // Store response ID and date for Responses API
590 if ( !empty( $extra['responseId'] ) ) {
591 $chatExtra['previousResponseId'] = $extra['responseId'];
592 $chatExtra['previousResponseDate'] = $now;
593 }
594
595 if ( $chat ) {
596 $chat->messages = json_decode( $chat->messages );
597 $userMessage = [ 'role' => 'user', 'content' => $newMessage ];
598 if ( $shortcutName ) {
599 $userMessage['shortcutName'] = $shortcutName;
600 $userMessage['shortcutPrompt'] = $query->get_message();
601 }
602 $chat->messages[] = $userMessage;
603 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
604 $chat->messages = json_encode( $chat->messages );
605
606 // Update or merge extra data
607 $existingExtra = json_decode( $chat->extra, true ) ?: [];
608 $mergedExtra = array_merge( $existingExtra, $chatExtra );
609
610 $this->wpdb->update(
611 $this->table_chats,
612 [
613 'userId' => $userId,
614 'messages' => $chat->messages,
615 'extra' => json_encode( $mergedExtra ),
616 'updated' => $now
617 ],
618 [ 'id' => $chat->id ]
619 );
620 }
621 else {
622 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
623 $messages = [];
624 if ( !empty( $startSentence ) ) {
625 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
626 }
627 $userMessage = [ 'role' => 'user', 'content' => $newMessage ];
628 if ( $shortcutName ) {
629 $userMessage['shortcutName'] = $shortcutName;
630 $userMessage['shortcutPrompt'] = $query->get_message();
631 }
632 $messages[] = $userMessage;
633 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
634 $chat = [
635 'userId' => $userId,
636 'ip' => $userIp,
637 'messages' => json_encode( $messages ),
638 'extra' => json_encode( $chatExtra ),
639 'botId' => $botId,
640 'chatId' => $chatId,
641 'threadId' => $threadId,
642 'storeId' => $storeId,
643 'created' => $now,
644 'updated' => $now
645 ];
646 $this->wpdb->insert( $this->table_chats, $chat );
647 }
648 return $rawText;
649 }
650
651 public function format_messages( $json, $format = 'html' ) {
652 $html = '';
653 if ( $format === 'html' ) {
654 try {
655 $conversation = json_decode( $json, true );
656 if ( json_last_error() !== JSON_ERROR_NONE ) {
657 return 'Invalid JSON format';
658 }
659 foreach ( $conversation as $message ) {
660 $role = ucfirst( $message['role'] );
661 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
662 }
663 }
664 catch ( Exception $e ) {
665 error_log( $e->getMessage() );
666 return 'Error while formatting the message';
667 }
668 }
669 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
670 return $html;
671 }
672
673 /**
674 * Commits a discussion into the database (create or update if the same chatId is found).
675 *
676 * @param Meow_MWAI_Discussion $discussionObject
677 * @return bool True if success, false if error
678 */
679 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
680 $this->check_db();
681
682 // 1. Check if a discussion with the same chatId already exists
683 $chat = $this->wpdb->get_row(
684 $this->wpdb->prepare(
685 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
686 $discussionObject->chatId
687 ),
688 ARRAY_A
689 );
690
691 // 2. Prepare data for DB
692 $userIp = $this->core->get_ip_address();
693 $userId = $this->core->get_user_id();
694 $now = date( 'Y-m-d H:i:s' );
695
696 $data = [
697 'userId' => $userId,
698 'ip' => $userIp,
699 'botId' => $discussionObject->botId,
700 'chatId' => $discussionObject->chatId,
701 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
702 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
703 'updated' => $now,
704 ];
705
706 // 3. Update if found, otherwise insert a new row
707 if ( $chat ) {
708 $updateRes = $this->wpdb->update(
709 $this->table_chats,
710 $data,
711 [ 'id' => $chat['id'] ]
712 );
713 if ( $updateRes === false ) {
714 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
715 return false;
716 }
717 }
718 else {
719 // For insertion, also set "created"
720 $data['created'] = $now;
721 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
722 if ( $insertRes === false ) {
723 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
724 return false;
725 }
726 }
727
728 return true;
729 }
730
731 public function check_db() {
732 if ( $this->db_check ) {
733 return true;
734 }
735
736 // Per-module version check: skip SHOW TABLES if already verified for this version.
737 if ( get_option( 'mwai_db_version_discussions' ) === MWAI_VERSION ) {
738 $this->db_check = true;
739 return true;
740 }
741
742 $this->db_check = !(
743 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
744 != strtolower( $this->table_chats )
745 );
746 if ( !$this->db_check ) {
747 $this->create_db();
748 $this->db_check = !(
749 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
750 != strtolower( $this->table_chats )
751 );
752 }
753
754 if ( $this->db_check ) {
755 // TODO: Remove after 2026-10-15. Upgrade legacy TEXT (64KB) messages column to MEDIUMTEXT (16MB)
756 // so long conversations aren't truncated on insert.
757 $column_info = $this->wpdb->get_row( "SHOW COLUMNS FROM $this->table_chats WHERE Field = 'messages'" );
758 if ( $column_info && stripos( $column_info->Type, 'mediumtext' ) === false
759 && stripos( $column_info->Type, 'longtext' ) === false ) {
760 $this->wpdb->query( "ALTER TABLE $this->table_chats MODIFY COLUMN messages MEDIUMTEXT NOT NULL" );
761 }
762 update_option( 'mwai_db_version_discussions', MWAI_VERSION, true );
763 }
764
765 return $this->db_check;
766 }
767
768 public function create_db() {
769 $charset_collate = $this->wpdb->get_charset_collate();
770 $sqlLogs = "CREATE TABLE $this->table_chats (
771 id BIGINT(20) NOT NULL AUTO_INCREMENT,
772 userId BIGINT(20) NULL,
773 ip VARCHAR(64) NULL,
774 title VARCHAR(64) NULL,
775 messages MEDIUMTEXT NOT NULL NULL,
776 extra LONGTEXT NOT NULL NULL,
777 botId VARCHAR(64) NULL,
778 chatId VARCHAR(64) NOT NULL,
779 threadId VARCHAR(64) NULL,
780 storeId VARCHAR(64) NULL,
781 created DATETIME NOT NULL,
782 updated DATETIME NOT NULL,
783 PRIMARY KEY (id),
784 INDEX chatId (chatId)
785 ) $charset_collate;";
786 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
787 dbDelta( $sqlLogs );
788 }
789
790 /**
791 * Handle cleanup task for discussions
792 */
793 public function handle_cleanup_task( $result, $job ) {
794 $start = microtime( true );
795 $retention_option = $this->core->get_option( 'chatbot_discussions_retention_days' );
796 // "Never" (or 0 / negative) disables the cleanup entirely.
797 if ( $retention_option === 'Never' || (int) $retention_option <= 0 ) {
798 return [
799 'ok' => true,
800 'done' => true,
801 'message' => 'Discussions cleanup disabled (retention set to Never)',
802 ];
803 }
804 $retention_days = (int) apply_filters( 'mwai_discussions_retention_days', (int) $retention_option );
805 if ( $retention_days <= 0 ) {
806 return [
807 'ok' => true,
808 'done' => true,
809 'message' => 'Discussions cleanup disabled by filter',
810 ];
811 }
812 $cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
813
814 // Check if discussions table exists
815 $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->table_chats}'" );
816 if ( !$table_exists ) {
817 return [
818 'ok' => true,
819 'done' => true,
820 'message' => 'Discussions table does not exist yet',
821 ];
822 }
823
824 // Get current progress
825 $deleted_total = isset( $job['meta']['deleted_total'] ) ? (int) $job['meta']['deleted_total'] : 0;
826 $last_id = isset( $job['meta']['last_id'] ) ? (int) $job['meta']['last_id'] : 0;
827
828 // Delete in batches
829 $batch_size = 100;
830 $deleted_batch = 0;
831
832 $old_discussions = $this->wpdb->get_results( $this->wpdb->prepare(
833 "SELECT id FROM {$this->table_chats}
834 WHERE updated < %s AND id > %d
835 ORDER BY id ASC
836 LIMIT %d",
837 $cutoff,
838 $last_id,
839 $batch_size
840 ) );
841
842 if ( !empty( $old_discussions ) ) {
843 $ids = wp_list_pluck( $old_discussions, 'id' );
844 $ids_string = implode( ',', array_map( 'intval', $ids ) );
845
846 $deleted_batch = $this->wpdb->query(
847 "DELETE FROM {$this->table_chats} WHERE id IN ($ids_string)"
848 );
849
850 $deleted_total += $deleted_batch;
851 $last_id = end( $ids );
852 }
853
854 // Check if we have more to process or time is running out
855 $has_more = count( $old_discussions ) === $batch_size;
856 $time_elapsed = microtime( true ) - $start;
857
858 if ( $has_more && $time_elapsed < 8 ) {
859 // Continue processing
860 return [
861 'ok' => true,
862 'done' => false,
863 'message' => sprintf( 'Deleted %d discussions (total: %d)', $deleted_batch, $deleted_total ),
864 'meta' => [
865 'deleted_total' => $deleted_total,
866 'last_id' => $last_id,
867 ],
868 'step' => $job['step'] + 1,
869 'step_name' => 'batch_' . ( $job['step'] + 1 ),
870 ];
871 }
872
873 // Completed
874 return [
875 'ok' => true,
876 'done' => true,
877 'message' => sprintf( 'Cleanup complete. Deleted %d discussions older than %d days', $deleted_total, $retention_days ),
878 'meta' => [
879 'deleted_total' => 0,
880 'last_id' => 0,
881 ],
882 ];
883 }
884 }
885