| @@ -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,104 +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 | 65 | |
| 130 | - // Cap and sanitize seed messages: only allowed roles, bounded content length, count capped. | |
| 131 | - $seedMessagesRaw = array_slice((array) $request->get('conversation_history', []), -self::MAX_SEED_MESSAGES); | |
| 132 | - $seedMessages = []; | |
| 133 | - foreach ($seedMessagesRaw as $m) { | |
| 134 | - if (!is_array($m)) { | |
| 135 | - continue; | |
| 136 | - } | |
| 137 | - $role = $m['role'] ?? ''; | |
| 138 | - if (!in_array($role, ['visitor', 'ai'], true)) { | |
| 139 | - continue; | |
| 140 | - } | |
| 141 | - $content = (string) ($m['content'] ?? ''); | |
| 142 | - if ($content === '') { | |
| 143 | - continue; | |
| 144 | - } | |
| 145 | - $seedMessages[] = [ | |
| 146 | - 'role' => $role, | |
| 147 | - 'content' => mb_substr($content, 0, self::MAX_SEED_MESSAGE_LENGTH), | |
| 148 | - ]; | |
| 149 | - } | |
| 150 | - | |
| 151 | - $resetChat = filter_var($request->get('reset_chat', false), FILTER_VALIDATE_BOOLEAN); | |
| 152 | - | |
| 153 | 66 | try { |
| 154 | 67 | $customAI = new FluentBotService(); |
| 155 | 68 | $ticket = Ticket::findOrFail($ticketId); |
| 156 | 69 | $this->ensureCanAccessTicket($ticket); |
| 157 | 70 | |
| 158 | - // Authorization passed — safe to touch ticket meta. | |
| 159 | - // If client requests a reset, clear stored chat mapping before reading. | |
| 160 | - // Otherwise only use the ticket's stored chat_id — ignore client-supplied values. | |
| 161 | - if ($resetChat) { | |
| 162 | - // Match the stricter permission gate used by deleteChatId — broader | |
| 163 | - // ticket-read access should not be enough to clear persisted chat state. | |
| 164 | - if (!(\FluentSupport\App\Modules\PermissionManager::canManageTickets() | |
| 165 | - || \FluentSupport\App\Modules\PermissionManager::currentUserCan('fst_draft_reply'))) { | |
| 166 | - throw new \Exception(__('You do not have permission to reset chat', 'fluent-support')); | |
| 167 | - } | |
| 168 | - $this->deleteTicketMeta($ticketId, '_fluent_bot_chat_id'); | |
| 169 | - $this->deleteTicketMeta($ticketId, '_fluent_bot_chat_product'); | |
| 170 | - $conversationId = ''; | |
| 171 | - } else { | |
| 172 | - $storedChat = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id'); | |
| 173 | - $conversationId = $storedChat ? $storedChat->value : ''; | |
| 174 | - // Guard against corrupt stored values; fall back to fresh chat upstream | |
| 175 | - if ($conversationId && !preg_match(self::CHAT_ID_PATTERN, $conversationId)) { | |
| 176 | - $conversationId = ''; | |
| 177 | - } | |
| 178 | - } | |
| 179 | - | |
| 180 | 71 | if ($type === 'modifyResponse') { |
| 181 | 72 | $result = $customAI->modifyResponse($prompt, $selectedText, $ticketId); |
| 182 | 73 | return $result; |
| 183 | 74 | } else { |
| 184 | - // Skip eager-load when selected-context mode is active — helper fetches targeted rows | |
| 185 | - if ($selectedConversations === null) { | |
| 186 | - $ticket->load('responses'); | |
| 187 | - } | |
| 75 | + $ticket->load('responses'); | |
| 188 | 76 | |
| 189 | - // Prevent WordPress and PHP from flushing/compressing buffers on shutdown | |
| 190 | - remove_action('shutdown', 'wp_ob_end_flush_all', 1); | |
| 191 | - @ini_set('zlib.output_compression', 'Off'); | |
| 192 | - @ini_set('output_buffering', 'Off'); | |
| 193 | - @ini_set('output_handler', ''); | |
| 194 | - | |
| 195 | - // Clear all output buffers for raw SSE streaming | |
| 77 | + // Disable all output buffering (with safety limit) | |
| 196 | 78 | $maxLevels = 10; |
| 197 | 79 | while (ob_get_level() && $maxLevels-- > 0) { |
| 198 | - @ob_end_clean(); | |
| 80 | + ob_end_clean(); | |
| 199 | 81 | } |
| 200 | 82 | |
| 83 | + // Set headers for Server-Sent Events | |
| 201 | 84 | header('Content-Type: text/event-stream'); |
| 202 | 85 | header('Cache-Control: no-cache'); |
| 203 | 86 | header('Connection: keep-alive'); |
| 204 | - 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'); | |
| 205 | 90 | |
| 91 | + // Disable WordPress output buffering | |
| 92 | + remove_action('shutdown', 'wp_ob_end_flush_all', 1); | |
| 93 | + | |
| 206 | 94 | // Send initial connection event |
| 207 | 95 | echo "event: connected\n"; |
| 208 | 96 | echo "data: Connection established\n\n"; |
| 209 | 97 | flush(); |
| @@ -208,9 +96,9 @@ | ||
| 208 | 96 | echo "data: Connection established\n\n"; |
| 209 | 97 | flush(); |
| 210 | 98 | |
| 211 | 99 | // Start streaming response |
| 212 | - $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null); | |
| 100 | + $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null); | |
| 213 | 101 | |
| 214 | 102 | // Send end event |
| 215 | 103 | echo "event: end\n"; |
| 216 | 104 | echo "data: Stream completed\n\n"; |
| @@ -218,23 +106,20 @@ | ||
| 218 | 106 | |
| 219 | 107 | exit; |
| 220 | 108 | } |
| 221 | 109 | } catch (\Exception $e) { |
| 222 | - // Send error as SSE event. Inline message extraction here because | |
| 223 | - // Helper::getSafeErrorMessage() throws ValidationException and would | |
| 224 | - // short-circuit the echo/flush/exit below. | |
| 225 | - $message = $e->getMessage() ?: __('Something went wrong. Please try again later.', 'fluent-support'); | |
| 110 | + // Send error as SSE event | |
| 226 | 111 | echo "event: error\n"; |
| 227 | - echo "data: " . json_encode(['message' => esc_html($message)]) . "\n\n"; | |
| 112 | + echo "data: " . json_encode(['message' => esc_html(Helper::getSafeErrorMessage($e))]) . "\n\n"; | |
| 228 | 113 | flush(); |
| 229 | 114 | exit; |
| 230 | 115 | } |
| 231 | 116 | } |
| 232 | 117 | |
| 233 | - public function getTicketSummary(Request $request, $id) | |
| 118 | + public function getTicketSummary(Request $request) | |
| 234 | 119 | { |
| 235 | 120 | try { |
| 236 | - $ticketId = intval($id); | |
| 121 | + $ticketId = $request->getSafe('id', 'intval'); | |
| 237 | 122 | $ticket = Ticket::with('responses')->findOrFail($ticketId); |
| 238 | 123 | $this->ensureCanAccessTicket($ticket); |
| 239 | 124 | |
| 240 | 125 | return (new FluentBotService())->getTicketSummary($ticket); |
| @@ -244,12 +129,12 @@ | ||
| 244 | 129 | ]); |
| 245 | 130 | } |
| 246 | 131 | } |
| 247 | 132 | |
| 248 | - public function getTicketTone(Request $request, $id) | |
| 133 | + public function getTicketTone(Request $request) | |
| 249 | 134 | { |
| 250 | 135 | try { |
| 251 | - $ticketId = intval($id); | |
| 136 | + $ticketId = $request->getSafe('id', 'intval'); | |
| 252 | 137 | $ticket = Ticket::with('responses')->findOrFail($ticketId); |
| 253 | 138 | $this->ensureCanAccessTicket($ticket); |
| 254 | 139 | |
| 255 | 140 | return (new FluentBotService())->getTicketTone($ticket); |
| @@ -259,267 +144,5 @@ | ||
| 259 | 144 | ]); |
| 260 | 145 | } |
| 261 | 146 | } |
| 262 | 147 | |
| 263 | - private function authorizeTicketAccess($ticketId) | |
| 264 | - { | |
| 265 | - $ticket = Ticket::findOrFail($ticketId); | |
| 266 | - $this->ensureCanAccessTicket($ticket); | |
| 267 | - return $ticket; | |
| 268 | - } | |
| 269 | - | |
| 270 | - public function getChatId(Request $request, $id) | |
| 271 | - { | |
| 272 | - $ticketId = intval($id); | |
| 273 | - $this->authorizeTicketAccess($ticketId); | |
| 274 | - | |
| 275 | - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id'); | |
| 276 | - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product'); | |
| 277 | - | |
| 278 | - return [ | |
| 279 | - 'chat_id' => $meta ? $meta->value : null, | |
| 280 | - 'product_id' => $productMeta ? (int) $productMeta->value : null | |
| 281 | - ]; | |
| 282 | - } | |
| 283 | - | |
| 284 | - public function getChatMessages(Request $request, $id) | |
| 285 | - { | |
| 286 | - $ticketId = intval($id); | |
| 287 | - $this->authorizeTicketAccess($ticketId); | |
| 288 | - $cursor = $request->getSafe('cursor', 'sanitize_text_field', ''); | |
| 289 | - | |
| 290 | - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id'); | |
| 291 | - | |
| 292 | - if (!$meta || !$meta->value) { | |
| 293 | - return ['data' => [], 'next_cursor' => null]; | |
| 294 | - } | |
| 295 | - | |
| 296 | - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product'); | |
| 297 | - $productId = $productMeta ? (int) $productMeta->value : $request->getSafe('product_id', 'intval'); | |
| 298 | - | |
| 299 | - $result = (new FluentBotService())->getChatMessages($meta->value, $productId, $cursor ?: null); | |
| 300 | - | |
| 301 | - if (is_wp_error($result)) { | |
| 302 | - return $this->sendError(['message' => $result->get_error_message()]); | |
| 303 | - } | |
| 304 | - | |
| 305 | - unset($result['path'], $result['prev_cursor'], $result['prev_page_url'], $result['next_page_url']); | |
| 306 | - | |
| 307 | - return $result; | |
| 308 | - } | |
| 309 | - | |
| 310 | - public function saveChatId(Request $request, $id) | |
| 311 | - { | |
| 312 | - $ticketId = intval($id); | |
| 313 | - $this->authorizeTicketAccess($ticketId); | |
| 314 | - $chatId = $request->getSafe('chat_id', 'sanitize_text_field'); | |
| 315 | - $productId = $request->getSafe('product_id', 'intval', 0); | |
| 316 | - | |
| 317 | - // Validate UUID format | |
| 318 | - if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) { | |
| 319 | - return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422); | |
| 320 | - } | |
| 321 | - | |
| 322 | - $db = \FluentSupport\App\App::getInstance('db'); | |
| 323 | - $result = $db->transaction(function () use ($ticketId, $chatId, $productId) { | |
| 324 | - // Lock the ticket row (always exists) to serialize concurrent first-writes for this ticket. | |
| 325 | - // lockForUpdate on a non-existent meta row is a no-op, so we anchor to the parent row instead. | |
| 326 | - Ticket::where('id', $ticketId)->lockForUpdate()->first(); | |
| 327 | - | |
| 328 | - $existing = Meta::where([ | |
| 329 | - 'object_type' => 'ticket_meta', | |
| 330 | - 'object_id' => $ticketId, | |
| 331 | - 'key' => '_fluent_bot_chat_id', | |
| 332 | - ])->orderByDesc('id')->first(); | |
| 333 | - | |
| 334 | - // Only conflict if the stored value is a valid UUID that differs from the new one. | |
| 335 | - // A corrupt/legacy stored value should be overwritten — the stream endpoint already | |
| 336 | - // treats it as empty, so keeping the 409 would trap tickets in a broken state. | |
| 337 | - if ($existing | |
| 338 | - && $existing->value | |
| 339 | - && preg_match(self::CHAT_ID_PATTERN, (string) $existing->value) | |
| 340 | - && $existing->value !== $chatId) { | |
| 341 | - return ['error' => __('Chat ID already set for this ticket', 'fluent-support')]; | |
| 342 | - } | |
| 343 | - | |
| 344 | - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_id', $chatId); | |
| 345 | - $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_product', $productId); | |
| 346 | - return null; | |
| 347 | - }); | |
| 348 | - | |
| 349 | - if ($result) { | |
| 350 | - return $this->sendError(['message' => $result['error']], 409); | |
| 351 | - } | |
| 352 | - | |
| 353 | - return [ | |
| 354 | - 'success' => true, | |
| 355 | - 'chat_id' => $chatId | |
| 356 | - ]; | |
| 357 | - } | |
| 358 | - | |
| 359 | - private function upsertTicketMeta($ticketId, $key, $value) | |
| 360 | - { | |
| 361 | - $where = [ | |
| 362 | - 'object_type' => 'ticket_meta', | |
| 363 | - 'object_id' => $ticketId, | |
| 364 | - 'key' => $key, | |
| 365 | - ]; | |
| 366 | - | |
| 367 | - $meta = Meta::where($where)->orderByDesc('id')->first(); | |
| 368 | - | |
| 369 | - if ($meta) { | |
| 370 | - $meta->value = $value; | |
| 371 | - $meta->save(); | |
| 372 | - } else { | |
| 373 | - Meta::create(array_merge($where, ['value' => $value])); | |
| 374 | - } | |
| 375 | - | |
| 376 | - // Do not prune siblings here — concurrent first-writes could race and delete each | |
| 377 | - // other's inserts, leaving zero rows. Reads use orderByDesc('id')->first() so | |
| 378 | - // duplicates are harmless at read time. saveChatId() serializes via ticket-row lock. | |
| 379 | - } | |
| 380 | - | |
| 381 | - private function getTicketMeta($ticketId, $key) | |
| 382 | - { | |
| 383 | - return Meta::where([ | |
| 384 | - 'object_type' => 'ticket_meta', | |
| 385 | - 'object_id' => $ticketId, | |
| 386 | - 'key' => $key, | |
| 387 | - ])->orderByDesc('id')->first(); | |
| 388 | - } | |
| 389 | - | |
| 390 | - private function deleteTicketMeta($ticketId, $key) | |
| 391 | - { | |
| 392 | - Meta::where([ | |
| 393 | - 'object_type' => 'ticket_meta', | |
| 394 | - 'object_id' => $ticketId, | |
| 395 | - 'key' => $key, | |
| 396 | - ])->delete(); | |
| 397 | - } | |
| 398 | - | |
| 399 | - public function deleteChatId(Request $request, $id) | |
| 400 | - { | |
| 401 | - $ticketId = intval($id); | |
| 402 | - $this->authorizeTicketAccess($ticketId); | |
| 403 | - | |
| 404 | - Meta::where('object_type', 'ticket_meta') | |
| 405 | - ->where('object_id', $ticketId) | |
| 406 | - ->whereIn('key', [ | |
| 407 | - '_fluent_bot_chat_id', | |
| 408 | - '_fluent_bot_chat_product', | |
| 409 | - '_fluent_bot_context_selection', | |
| 410 | - ]) | |
| 411 | - ->delete(); | |
| 412 | - | |
| 413 | - return [ | |
| 414 | - 'success' => true | |
| 415 | - ]; | |
| 416 | - } | |
| 417 | - | |
| 418 | - public function getContextSelection(Request $request, $id) | |
| 419 | - { | |
| 420 | - $ticketId = intval($id); | |
| 421 | - $this->authorizeTicketAccess($ticketId); | |
| 422 | - | |
| 423 | - $meta = $this->getTicketMeta($ticketId, '_fluent_bot_context_selection'); | |
| 424 | - | |
| 425 | - if (!$meta) { | |
| 426 | - return ['data' => null]; | |
| 427 | - } | |
| 428 | - | |
| 429 | - return ['data' => Helper::safeUnserialize($meta->value)]; | |
| 430 | - } | |
| 431 | - | |
| 432 | - public function saveContextSelection(Request $request, $id) | |
| 433 | - { | |
| 434 | - $ticketId = intval($id); | |
| 435 | - $this->authorizeTicketAccess($ticketId); | |
| 436 | - $selectedIds = (array) $request->get('selected_ids', []); | |
| 437 | - $knownIds = (array) $request->get('known_ids', []); | |
| 438 | - $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN); | |
| 439 | - | |
| 440 | - // Cap array sizes to prevent meta-row bloat from malicious input | |
| 441 | - $data = [ | |
| 442 | - 'selectedIds' => array_slice(array_map('intval', $selectedIds), 0, 500), | |
| 443 | - 'knownIds' => array_slice(array_map('intval', $knownIds), 0, 500), | |
| 444 | - 'includeTicketContent' => $includeTicketContent, | |
| 445 | - ]; | |
| 446 | - | |
| 447 | - $serialized = maybe_serialize($data); | |
| 448 | - | |
| 449 | - $this->upsertTicketMeta($ticketId, '_fluent_bot_context_selection', $serialized); | |
| 450 | - | |
| 451 | - return ['success' => true]; | |
| 452 | - } | |
| 453 | - | |
| 454 | - private function resolveTicketFeedbackContext($ticketId) | |
| 455 | - { | |
| 456 | - $chatMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id'); | |
| 457 | - if (!$chatMeta || !$chatMeta->value) { | |
| 458 | - return null; | |
| 459 | - } | |
| 460 | - | |
| 461 | - $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product'); | |
| 462 | - return [ | |
| 463 | - 'chat_id' => $chatMeta->value, | |
| 464 | - 'product_id' => $productMeta ? (int) $productMeta->value : 0, | |
| 465 | - ]; | |
| 466 | - } | |
| 467 | - | |
| 468 | - public function createFeedback(Request $request, $id) | |
| 469 | - { | |
| 470 | - $ticketId = intval($id); | |
| 471 | - $this->authorizeTicketAccess($ticketId); | |
| 472 | - | |
| 473 | - $messageId = $request->getSafe('message_id', 'intval'); | |
| 474 | - $reaction = $request->getSafe('reaction', 'sanitize_text_field'); | |
| 475 | - $comment = $request->getSafe('comments', 'sanitize_textarea_field', ''); | |
| 476 | - | |
| 477 | - if (!$messageId || $messageId < 1) { | |
| 478 | - return $this->sendError(['message' => __('Invalid message_id', 'fluent-support')], 422); | |
| 479 | - } | |
| 480 | - | |
| 481 | - if (!$reaction || !in_array($reaction, ['positive', 'negative'], true)) { | |
| 482 | - return $this->sendError(['message' => __('Invalid reaction', 'fluent-support')], 422); | |
| 483 | - } | |
| 484 | - | |
| 485 | - $ctx = $this->resolveTicketFeedbackContext($ticketId); | |
| 486 | - if (!$ctx) { | |
| 487 | - return $this->sendError(['message' => __('No active chat for this ticket', 'fluent-support')], 422); | |
| 488 | - } | |
| 489 | - | |
| 490 | - $helper = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotHelper(); | |
| 491 | - $result = $helper->createFeedback($messageId, $reaction, $comment ?: null, $ctx['product_id'], $ctx['chat_id']); | |
| 492 | - | |
| 493 | - if (is_wp_error($result)) { | |
| 494 | - return $this->sendError(['message' => $result->get_error_message()], 500); | |
| 495 | - } | |
| 496 | - | |
| 497 | - return $result; | |
| 498 | - } | |
| 499 | - | |
| 500 | - public function deleteFeedback(Request $request, $id, $feedback_id) | |
| 501 | - { | |
| 502 | - $ticketId = intval($id); | |
| 503 | - $this->authorizeTicketAccess($ticketId); | |
| 504 | - | |
| 505 | - $feedbackId = intval($feedback_id); | |
| 506 | - | |
| 507 | - if (!$feedbackId || $feedbackId < 1) { | |
| 508 | - return $this->sendError(['message' => __('Invalid feedback_id', 'fluent-support')], 422); | |
| 509 | - } | |
| 510 | - | |
| 511 | - $ctx = $this->resolveTicketFeedbackContext($ticketId); | |
| 512 | - if (!$ctx) { | |
| 513 | - return $this->sendError(['message' => __('No active chat for this ticket', 'fluent-support')], 422); | |
| 514 | - } | |
| 515 | - | |
| 516 | - $helper = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotHelper(); | |
| 517 | - $result = $helper->deleteFeedback($feedbackId, $ctx['product_id'], $ctx['chat_id']); | |
| 518 | - | |
| 519 | - if (is_wp_error($result)) { | |
| 520 | - return $this->sendError(['message' => $result->get_error_message()], 500); | |
| 521 | - } | |
| 522 | - | |
| 523 | - return $result; | |
| 524 | - } | |
| 525 | 148 | } |