PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.0
2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 All 67 releases
← All changes | app/Http/Controllers/FluentBotController.php +22 -497 2.3.22.1.0 View file →
@@ -5,19 +5,13 @@
5 5
6 6 use FluentSupport\Framework\Http\Request\Request;
7 7 use FluentSupport\App\Http\Controllers\Controller;
8 8 use FluentSupport\App\Models\Ticket;
9 -use FluentSupport\App\Models\Meta;
10 9 use FluentSupport\App\Services\Helper;
11 10 use FluentSupport\App\Services\Integrations\FluentBot\FluentBotService;
12 11
13 12 class FluentBotController extends Controller
14 13 {
15 - private const CHAT_ID_PATTERN = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i';
16 - private const MAX_SELECTED_CONVERSATIONS = 500;
17 - private const MAX_SEED_MESSAGES = 50;
18 - private const MAX_SEED_MESSAGE_LENGTH = 5000;
19 -
20 14 public function getPresetPrompts(Request $request)
21 15 {
22 16 $type = $request->getSafe('type', 'sanitize_text_field');
23 17
@@ -29,58 +23,11 @@
29 23 ]);
30 24 }
31 25 }
32 26
33 - // Ticket-safe runtime config for the chat panel: tells the UI which products have a bot
34 - // configured and whether a general bot exists. Never exposes bot IDs. Readable by any
35 - // agent with ticket-route access so the panel works without AdminSettingsPolicy.
36 - public function getRuntimeConfig()
27 + public function generateResponse(Request $request)
37 28 {
38 - $meta = Meta::where([
39 - 'object_type' => 'fluent_bot_settings',
40 - 'object_id' => 1,
41 - 'key' => '_fs_fluent_bot_config'
42 - ])->orderByDesc('id')->first();
43 -
44 - $settings = $meta ? Helper::safeUnserialize($meta->value) : [];
45 - if (!is_array($settings)) {
46 - $settings = [];
47 - }
48 -
49 - // generalBotEnabled defaults to true for backward compat with configs saved
50 - // before the flag existed. The runtime-config consumer uses this to decide
51 - // whether to show the "General Bot" option in the ticket-side product dropdown.
52 - $generalBotEnabled = !array_key_exists('generalBotEnabled', $settings)
53 - || filter_var($settings['generalBotEnabled'], FILTER_VALIDATE_BOOLEAN);
54 -
55 - $hasGeneralBot = $generalBotEnabled
56 - && !empty($settings['generalBotId'])
57 - && trim((string) $settings['generalBotId']) !== '';
58 -
59 - $configuredProductIds = [];
60 - foreach (($settings['productMappings'] ?? []) as $mapping) {
61 - if (!is_array($mapping)) {
62 - continue;
63 - }
64 - $botId = $mapping['botId'] ?? '';
65 - if (trim((string) $botId) === '') {
66 - continue;
67 - }
68 - $productId = intval($mapping['productId'] ?? 0);
69 - if ($productId > 0) {
70 - $configuredProductIds[] = $productId;
71 - }
72 - }
73 -
74 - return [
75 - 'hasGeneralBot' => $hasGeneralBot,
76 - 'configuredProductIds' => array_values(array_unique($configuredProductIds)),
77 - ];
78 - }
79 -
80 - public function generateResponse(Request $request, $id)
81 - {
82 - $ticketId = intval($id);
29 + $ticketId = $request->getSafe('id', 'intval');
83 30 $productId = $request->getSafe('product_id', 'intval');
84 31 $prompt = $request->getSafe('content', 'sanitize_text_field');
85 32 $conversationId = $request->getSafe('chat_id', 'sanitize_text_field', '');
86 33 $selectedText = $request->getSafe('selectedText', 'sanitize_text_field', '');
@@ -106,106 +53,45 @@
106 53 ]);
107 54 }
108 55 }
109 56
110 -
111 -
112 - public function generateStreamResponse(Request $request, $id)
57 + public function generateStreamResponse(Request $request)
113 58 {
114 - $ticketId = intval($id);
59 + $ticketId = $request->getSafe('id', 'intval');
115 60 $productId = $request->getSafe('product_id', 'intval');
116 61 $prompt = $request->getSafe('content', 'sanitize_text_field');
62 + $conversationId = $request->getSafe('chat_id', 'sanitize_text_field', '');
117 63 $selectedText = $request->getSafe('selectedText', 'sanitize_text_field', '');
118 64 $type = $request->getSafe('type', 'sanitize_text_field', 'response');
119 - // Distinguish "key absent" (use full ticket context) from "explicit empty list"
120 - // (user intentionally deselected all responses — keep selected mode).
121 - // Normalize to non-empty int IDs and cap length to prevent abuse.
122 - $selectedConversations = $request->exists('selected_conversations')
123 - ? array_slice(
124 - array_values(array_filter(array_map('intval', (array) $request->get('selected_conversations', [])))),
125 - 0, self::MAX_SELECTED_CONVERSATIONS
126 - )
127 - : null;
128 - $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN);
129 - $webSearch = filter_var($request->get('web_search', false), FILTER_VALIDATE_BOOLEAN);
130 - $temperature = max(0, min(2, floatval($request->get('temperature', 0))));
131 65
132 - // Cap and sanitize seed messages: only allowed roles, bounded content length, count capped.
133 - $seedMessagesRaw = array_slice((array) $request->get('conversation_history', []), -self::MAX_SEED_MESSAGES);
134 - $seedMessages = [];
135 - foreach ($seedMessagesRaw as $m) {
136 - if (!is_array($m)) {
137 - continue;
138 - }
139 - $role = $m['role'] ?? '';
140 - if (!in_array($role, ['visitor', 'ai'], true)) {
141 - continue;
142 - }
143 - $content = (string) ($m['content'] ?? '');
144 - if ($content === '') {
145 - continue;
146 - }
147 - $seedMessages[] = [
148 - 'role' => $role,
149 - 'content' => mb_substr($content, 0, self::MAX_SEED_MESSAGE_LENGTH),
150 - ];
151 - }
152 -
153 - $resetChat = filter_var($request->get('reset_chat', false), FILTER_VALIDATE_BOOLEAN);
154 -
155 66 try {
156 67 $customAI = new FluentBotService();
157 68 $ticket = Ticket::findOrFail($ticketId);
158 69 $this->ensureCanAccessTicket($ticket);
159 70
160 - // Authorization passed — safe to touch ticket meta.
161 - // If client requests a reset, clear stored chat mapping before reading.
162 - // Otherwise only use the ticket's stored chat_id — ignore client-supplied values.
163 - if ($resetChat) {
164 - // Match the stricter permission gate used by deleteChatId — broader
165 - // ticket-read access should not be enough to clear persisted chat state.
166 - if (!(\FluentSupport\App\Modules\PermissionManager::canManageTickets()
167 - || \FluentSupport\App\Modules\PermissionManager::currentUserCan('fst_draft_reply'))) {
168 - throw new \Exception(__('You do not have permission to reset chat', 'fluent-support'));
169 - }
170 - $this->deleteTicketMeta($ticketId, '_fluent_bot_chat_id');
171 - $this->deleteTicketMeta($ticketId, '_fluent_bot_chat_product');
172 - $conversationId = '';
173 - } else {
174 - $storedChat = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
175 - $conversationId = $storedChat ? $storedChat->value : '';
176 - // Guard against corrupt stored values; fall back to fresh chat upstream
177 - if ($conversationId && !preg_match(self::CHAT_ID_PATTERN, $conversationId)) {
178 - $conversationId = '';
179 - }
180 - }
181 -
182 71 if ($type === 'modifyResponse') {
183 72 $result = $customAI->modifyResponse($prompt, $selectedText, $ticketId);
184 73 return $result;
185 74 } else {
186 - // Skip eager-load when selected-context mode is active — helper fetches targeted rows
187 - if ($selectedConversations === null) {
188 - $ticket->load('responses');
189 - }
75 + $ticket->load('responses');
190 76
191 - // Prevent WordPress and PHP from flushing/compressing buffers on shutdown
192 - remove_action('shutdown', 'wp_ob_end_flush_all', 1);
193 - @ini_set('zlib.output_compression', 'Off');
194 - @ini_set('output_buffering', 'Off');
195 - @ini_set('output_handler', '');
196 -
197 - // Clear all output buffers for raw SSE streaming
77 + // Disable all output buffering (with safety limit)
198 78 $maxLevels = 10;
199 79 while (ob_get_level() && $maxLevels-- > 0) {
200 - @ob_end_clean();
80 + ob_end_clean();
201 81 }
202 82
83 + // Set headers for Server-Sent Events
203 84 header('Content-Type: text/event-stream');
204 85 header('Cache-Control: no-cache');
205 86 header('Connection: keep-alive');
206 - header('X-Accel-Buffering: no');
87 + header('X-Accel-Buffering: no'); // Disable nginx buffering
88 + header('Access-Control-Allow-Origin: *');
89 + header('Access-Control-Allow-Headers: Cache-Control');
207 90
91 + // Disable WordPress output buffering
92 + remove_action('shutdown', 'wp_ob_end_flush_all', 1);
93 +
208 94 // Send initial connection event
209 95 echo "event: connected\n";
210 96 echo "data: Connection established\n\n";
211 97 flush();
@@ -210,9 +96,9 @@
210 96 echo "data: Connection established\n\n";
211 97 flush();
212 98
213 99 // Start streaming response
214 - $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null, $webSearch, $temperature);
100 + $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null);
215 101
216 102 // Send end event
217 103 echo "event: end\n";
218 104 echo "data: Stream completed\n\n";
@@ -220,23 +106,20 @@
220 106
221 107 exit;
222 108 }
223 109 } catch (\Exception $e) {
224 - // Send error as SSE event. Inline message extraction here because
225 - // Helper::getSafeErrorMessage() throws ValidationException and would
226 - // short-circuit the echo/flush/exit below.
227 - $message = $e->getMessage() ?: __('Something went wrong. Please try again later.', 'fluent-support');
110 + // Send error as SSE event
228 111 echo "event: error\n";
229 - echo "data: " . json_encode(['message' => esc_html($message)]) . "\n\n";
112 + echo "data: " . json_encode(['message' => esc_html(Helper::getSafeErrorMessage($e))]) . "\n\n";
230 113 flush();
231 114 exit;
232 115 }
233 116 }
234 117
235 - public function getTicketSummary(Request $request, $id)
118 + public function getTicketSummary(Request $request)
236 119 {
237 120 try {
238 - $ticketId = intval($id);
121 + $ticketId = $request->getSafe('id', 'intval');
239 122 $ticket = Ticket::with('responses')->findOrFail($ticketId);
240 123 $this->ensureCanAccessTicket($ticket);
241 124
242 125 return (new FluentBotService())->getTicketSummary($ticket);
@@ -246,12 +129,12 @@
246 129 ]);
247 130 }
248 131 }
249 132
250 - public function getTicketTone(Request $request, $id)
133 + public function getTicketTone(Request $request)
251 134 {
252 135 try {
253 - $ticketId = intval($id);
136 + $ticketId = $request->getSafe('id', 'intval');
254 137 $ticket = Ticket::with('responses')->findOrFail($ticketId);
255 138 $this->ensureCanAccessTicket($ticket);
256 139
257 140 return (new FluentBotService())->getTicketTone($ticket);
@@ -261,363 +144,5 @@
261 144 ]);
262 145 }
263 146 }
264 147
265 - private function authorizeTicketAccess($ticketId)
266 - {
267 - $ticket = Ticket::findOrFail($ticketId);
268 - $this->ensureCanAccessTicket($ticket);
269 - return $ticket;
270 - }
271 -
272 - public function getChatId(Request $request, $id)
273 - {
274 - $ticketId = intval($id);
275 - $this->authorizeTicketAccess($ticketId);
276 -
277 - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
278 - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
279 -
280 - return [
281 - 'chat_id' => $meta ? $meta->value : null,
282 - 'product_id' => $productMeta ? (int) $productMeta->value : null
283 - ];
284 - }
285 -
286 - public function getChatMessages(Request $request, $id)
287 - {
288 - $ticketId = intval($id);
289 - $this->authorizeTicketAccess($ticketId);
290 - $cursor = $request->getSafe('cursor', 'sanitize_text_field', '');
291 -
292 - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
293 -
294 - if (!$meta || !$meta->value) {
295 - return ['data' => [], 'next_cursor' => null];
296 - }
297 -
298 - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
299 - $productId = $productMeta ? (int) $productMeta->value : $request->getSafe('product_id', 'intval');
300 -
301 - $result = (new FluentBotService())->getChatMessages($meta->value, $productId, $cursor ?: null);
302 -
303 - if (is_wp_error($result)) {
304 - return $this->sendError(['message' => $result->get_error_message()]);
305 - }
306 -
307 - unset($result['path'], $result['prev_cursor'], $result['prev_page_url'], $result['next_page_url']);
308 -
309 - return $result;
310 - }
311 -
312 - public function saveChatId(Request $request, $id)
313 - {
314 - $ticketId = intval($id);
315 - $this->authorizeTicketAccess($ticketId);
316 - $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
317 - $productId = $request->getSafe('product_id', 'intval', 0);
318 - $title = $request->getSafe('title', 'sanitize_text_field', '');
319 -
320 - // Validate UUID format
321 - if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
322 - return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
323 - }
324 -
325 - $db = \FluentSupport\App\App::getInstance('db');
326 - $result = $db->transaction(function () use ($ticketId, $chatId, $productId) {
327 - // Lock the ticket row (always exists) to serialize concurrent first-writes for this ticket.
328 - // lockForUpdate on a non-existent meta row is a no-op, so we anchor to the parent row instead.
329 - Ticket::where('id', $ticketId)->lockForUpdate()->first();
330 -
331 - $existing = Meta::where([
332 - 'object_type' => 'ticket_meta',
333 - 'object_id' => $ticketId,
334 - 'key' => '_fluent_bot_chat_id',
335 - ])->orderByDesc('id')->first();
336 -
337 - // Only conflict if the stored value is a valid UUID that differs from the new one.
338 - // A corrupt/legacy stored value should be overwritten — the stream endpoint already
339 - // treats it as empty, so keeping the 409 would trap tickets in a broken state.
340 - if ($existing
341 - && $existing->value
342 - && preg_match(self::CHAT_ID_PATTERN, (string) $existing->value)
343 - && $existing->value !== $chatId) {
344 - return ['error' => __('Chat ID already set for this ticket', 'fluent-support')];
345 - }
346 -
347 - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_id', $chatId);
348 - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_product', $productId);
349 - return null;
350 - });
351 -
352 - if ($result) {
353 - return $this->sendError(['message' => $result['error']], 409);
354 - }
355 -
356 - // Track the conversation so it appears in the ticket's "Past conversations"
357 - // list. Idempotent per chat_id — repeated saves of the same id are no-ops.
358 - $this->appendChatHistory($ticketId, $chatId, $productId, $title);
359 -
360 - return [
361 - 'success' => true,
362 - 'chat_id' => $chatId
363 - ];
364 - }
365 -
366 - private function upsertTicketMeta($ticketId, $key, $value)
367 - {
368 - $where = [
369 - 'object_type' => 'ticket_meta',
370 - 'object_id' => $ticketId,
371 - 'key' => $key,
372 - ];
373 -
374 - $meta = Meta::where($where)->orderByDesc('id')->first();
375 -
376 - if ($meta) {
377 - $meta->value = $value;
378 - $meta->save();
379 - } else {
380 - Meta::create(array_merge($where, ['value' => $value]));
381 - }
382 -
383 - // Do not prune siblings here — concurrent first-writes could race and delete each
384 - // other's inserts, leaving zero rows. Reads use orderByDesc('id')->first() so
385 - // duplicates are harmless at read time. saveChatId() serializes via ticket-row lock.
386 - }
387 -
388 - private function getTicketMeta($ticketId, $key)
389 - {
390 - return Meta::where([
391 - 'object_type' => 'ticket_meta',
392 - 'object_id' => $ticketId,
393 - 'key' => $key,
394 - ])->orderByDesc('id')->first();
395 - }
396 -
397 - private function deleteTicketMeta($ticketId, $key)
398 - {
399 - Meta::where([
400 - 'object_type' => 'ticket_meta',
401 - 'object_id' => $ticketId,
402 - 'key' => $key,
403 - ])->delete();
404 - }
405 -
406 - public function deleteChatId(Request $request, $id)
407 - {
408 - $ticketId = intval($id);
409 - $this->authorizeTicketAccess($ticketId);
410 -
411 - Meta::where('object_type', 'ticket_meta')
412 - ->where('object_id', $ticketId)
413 - ->whereIn('key', [
414 - '_fluent_bot_chat_id',
415 - '_fluent_bot_chat_product',
416 - '_fluent_bot_context_selection',
417 - ])
418 - ->delete();
419 -
420 - return [
421 - 'success' => true
422 - ];
423 - }
424 -
425 - /**
426 - * List the ticket's past FluentBot conversations (newest first) plus the
427 - * currently-active chat_id, for the "Past conversations" switcher.
428 - */
429 - public function getConversations(Request $request, $id)
430 - {
431 - $ticketId = intval($id);
432 - $this->authorizeTicketAccess($ticketId);
433 -
434 - $active = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
435 -
436 - return [
437 - 'conversations' => $this->readChatHistory($ticketId),
438 - 'active_chat_id' => $active ? $active->value : null,
439 - ];
440 - }
441 -
442 - /**
443 - * Make a past conversation the active one so the next message continues it.
444 - * Only a chat_id already recorded in THIS ticket's history may be selected —
445 - * the stream endpoint trusts the stored chat_id, so this is the ownership gate.
446 - */
447 - public function switchConversation(Request $request, $id)
448 - {
449 - $ticketId = intval($id);
450 - $this->authorizeTicketAccess($ticketId);
451 - $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
452 -
453 - if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
454 - return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
455 - }
456 -
457 - $entry = null;
458 - foreach ($this->readChatHistory($ticketId) as $h) {
459 - if (isset($h['chat_id']) && $h['chat_id'] === $chatId) {
460 - $entry = $h;
461 - break;
462 - }
463 - }
464 -
465 - if (!$entry) {
466 - return $this->sendError(['message' => __('Conversation not found for this ticket.', 'fluent-support')], 404);
467 - }
468 -
469 - $productId = (int) ($entry['product_id'] ?? 0);
470 - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_id', $chatId);
471 - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_product', $productId);
472 -
473 - return [
474 - 'success' => true,
475 - 'chat_id' => $chatId,
476 - 'product_id' => $productId,
477 - ];
478 - }
479 -
480 - private function readChatHistory($ticketId): array
481 - {
482 - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_history');
483 - $history = ($meta && $meta->value) ? json_decode($meta->value, true) : [];
484 -
485 - return is_array($history) ? array_values($history) : [];
486 - }
487 -
488 - /**
489 - * Prepend a conversation to the ticket's history. Idempotent per chat_id;
490 - * capped so ticket meta cannot grow unbounded.
491 - */
492 - private function appendChatHistory($ticketId, $chatId, $productId, $title = '')
493 - {
494 - $history = $this->readChatHistory($ticketId);
495 -
496 - foreach ($history as $entry) {
497 - if (isset($entry['chat_id']) && $entry['chat_id'] === $chatId) {
498 - return;
499 - }
500 - }
501 -
502 - array_unshift($history, [
503 - 'chat_id' => $chatId,
504 - 'product_id' => (int) $productId,
505 - 'title' => $title !== '' ? $title : __('Conversation', 'fluent-support'),
506 - 'created_at' => time(),
507 - ]);
508 -
509 - $history = array_slice($history, 0, 20);
510 -
511 - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_history', wp_json_encode($history));
512 - }
513 -
514 - public function getContextSelection(Request $request, $id)
515 - {
516 - $ticketId = intval($id);
517 - $this->authorizeTicketAccess($ticketId);
518 -
519 - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_context_selection');
520 -
521 - if (!$meta) {
522 - return ['data' => null];
523 - }
524 -
525 - return ['data' => Helper::safeUnserialize($meta->value)];
526 - }
527 -
528 - public function saveContextSelection(Request $request, $id)
529 - {
530 - $ticketId = intval($id);
531 - $this->authorizeTicketAccess($ticketId);
532 - $selectedIds = (array) $request->get('selected_ids', []);
533 - $knownIds = (array) $request->get('known_ids', []);
534 - $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN);
535 -
536 - // Cap array sizes to prevent meta-row bloat from malicious input
537 - $data = [
538 - 'selectedIds' => array_slice(array_map('intval', $selectedIds), 0, 500),
539 - 'knownIds' => array_slice(array_map('intval', $knownIds), 0, 500),
540 - 'includeTicketContent' => $includeTicketContent,
541 - ];
542 -
543 - $serialized = maybe_serialize($data);
544 -
545 - $this->upsertTicketMeta($ticketId, '_fluent_bot_context_selection', $serialized);
546 -
547 - return ['success' => true];
548 - }
549 -
550 - private function resolveTicketFeedbackContext($ticketId)
551 - {
552 - $chatMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
553 - if (!$chatMeta || !$chatMeta->value) {
554 - return null;
555 - }
556 -
557 - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
558 - return [
559 - 'chat_id' => $chatMeta->value,
560 - 'product_id' => $productMeta ? (int) $productMeta->value : 0,
561 - ];
562 - }
563 -
564 - public function createFeedback(Request $request, $id)
565 - {
566 - $ticketId = intval($id);
567 - $this->authorizeTicketAccess($ticketId);
568 -
569 - // fluent-bot Message PKs are UUIDs — sanitize as text, not intval.
570 - $messageId = $request->getSafe('message_id', 'sanitize_text_field');
571 - $reaction = $request->getSafe('reaction', 'sanitize_text_field');
572 - $comment = $request->getSafe('comments', 'sanitize_textarea_field', '');
573 -
574 - if (!$messageId || !wp_is_uuid($messageId)) {
575 - return $this->sendError(['message' => __('Invalid message_id', 'fluent-support')], 422);
576 - }
577 -
578 - if (!$reaction || !in_array($reaction, ['positive', 'negative'], true)) {
579 - return $this->sendError(['message' => __('Invalid reaction', 'fluent-support')], 422);
580 - }
581 -
582 - $ctx = $this->resolveTicketFeedbackContext($ticketId);
583 - if (!$ctx) {
584 - return $this->sendError(['message' => __('No active chat for this ticket', 'fluent-support')], 422);
585 - }
586 -
587 - $helper = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotHelper();
588 - $result = $helper->createFeedback($messageId, $reaction, $comment ?: null, $ctx['product_id'], $ctx['chat_id']);
589 -
590 - if (is_wp_error($result)) {
591 - return $this->sendError(['message' => $result->get_error_message()], 500);
592 - }
593 -
594 - return $result;
595 - }
596 -
597 - public function deleteFeedback(Request $request, $id, $feedback_id)
598 - {
599 - $ticketId = intval($id);
600 - $this->authorizeTicketAccess($ticketId);
601 -
602 - // fluent-bot Feedback PKs are integers (unlike message ids, which are UUIDs).
603 - $feedbackId = intval($feedback_id);
604 -
605 - if (!$feedbackId || $feedbackId < 1) {
606 - return $this->sendError(['message' => __('Invalid feedback_id', 'fluent-support')], 422);
607 - }
608 -
609 - $ctx = $this->resolveTicketFeedbackContext($ticketId);
610 - if (!$ctx) {
611 - return $this->sendError(['message' => __('No active chat for this ticket', 'fluent-support')], 422);
612 - }
613 -
614 - $helper = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotHelper();
615 - $result = $helper->deleteFeedback($feedbackId, $ctx['product_id'], $ctx['chat_id']);
616 -
617 - if (is_wp_error($result)) {
618 - return $this->sendError(['message' => $result->get_error_message()], 500);
619 - }
620 -
621 - return $result;
622 - }
623 148 }