resolveConversation($request); $messages = $this->conversationMessages($conversation); $conversation->messages() ->where('sender', 'admin') ->where('is_read', false) ->update(['is_read' => true]); return response()->json([ 'conversation' => $this->conversationPayload($conversation), 'conversation_id' => $conversation->id, 'messages' => $messages->map(fn (ChatMessage $message) => $this->messagePayload($message))->values(), ]); } public function store(Request $request): JsonResponse { $validated = $request->validate([ 'message' => ['required', 'string', 'max:2000'], ]); $messageText = $this->sanitizeMessage((string) $validated['message']); if ($messageText === '') { throw ValidationException::withMessages([ 'message' => 'Сообщение содержит недопустимые символы.', ]); } $conversation = $this->resolveConversationForWriting($request); $message = $conversation->messages()->create([ 'sender' => 'customer', 'body' => $messageText, 'is_read' => false, ]); $conversation->update([ 'status' => 'open', 'last_message_at' => now(), ]); return response()->json([ 'conversation' => $this->conversationPayload($conversation), 'message' => $this->messagePayload($message), ]); } private function resolveConversation(Request $request): ChatConversation { $token = $this->sessionToken($request); $user = $request->user(); if (!$user) { $conversation = ChatConversation::query()->where('visitor_token', $token)->first(); if ($conversation && $conversation->user_id !== null) { $token = $this->rotateSessionToken($request); $conversation = null; } return $conversation ?: ChatConversation::query()->firstOrCreate( ['visitor_token' => $token], ['status' => 'open'] ); } $userConversation = ChatConversation::query() ->where('user_id', $user->id) ->latest('id') ->first(); if ($userConversation) { return $userConversation; } $guestConversation = ChatConversation::query() ->where('visitor_token', $token) ->whereNull('user_id') ->first(); if ($guestConversation) { $guestConversation->update(['user_id' => $user->id]); return $guestConversation; } return ChatConversation::query()->create([ 'user_id' => $user->id, 'visitor_token' => $this->generateUniqueToken(), 'status' => ChatConversation::STATUS_OPEN, ]); } private function resolveConversationForWriting(Request $request): ChatConversation { $conversation = $this->resolveConversation($request); if (!$conversation->isClosed()) { return $conversation; } $user = $request->user(); if (!$user) { return ChatConversation::query()->create([ 'visitor_token' => $this->rotateSessionToken($request), 'status' => ChatConversation::STATUS_OPEN, ]); } return ChatConversation::query()->create([ 'user_id' => $user->id, 'visitor_token' => $this->generateUniqueToken(), 'status' => ChatConversation::STATUS_OPEN, ]); } private function conversationMessages(ChatConversation $conversation) { return $conversation->messages() ->latest('id') ->limit(100) ->get() ->reverse() ->values(); } private function messagePayload(ChatMessage $message): array { return [ 'id' => $message->id, 'sender' => $message->sender, 'body' => $message->body, 'created_at' => $message->created_at?->toIso8601String(), 'time' => $message->created_at?->format('H:i'), ]; } private function conversationPayload(ChatConversation $conversation): array { return [ 'id' => $conversation->id, 'status' => $conversation->status, 'is_closed' => $conversation->isClosed(), 'notice' => $conversation->isClosed() ? 'Чат закрыт администратором. Отправьте новое сообщение, чтобы начать новый диалог.' : null, ]; } private function sanitizeMessage(string $value): string { $text = strip_tags($value); $text = str_replace(["\r\n", "\r"], "\n", $text); $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text) ?? $text; return trim(Str::limit($text, 2000, '')); } private function sessionToken(Request $request): string { $token = (string) $request->session()->get(self::SESSION_TOKEN_KEY, ''); if ($token !== '') { return $token; } return $this->rotateSessionToken($request); } private function rotateSessionToken(Request $request): string { $token = $this->generateUniqueToken(); $request->session()->put(self::SESSION_TOKEN_KEY, $token); return $token; } private function generateUniqueToken(): string { do { $token = (string) Str::uuid(); } while (ChatConversation::query()->where('visitor_token', $token)->exists()); return $token; } }