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