200 lines
6.2 KiB
PHP
200 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Shop;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ChatConversation;
|
|
use App\Models\ChatMessage;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ChatController extends Controller
|
|
{
|
|
private const SESSION_TOKEN_KEY = 'chat.visitor_token';
|
|
|
|
public function messages(Request $request): JsonResponse
|
|
{
|
|
$conversation = $this->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;
|
|
}
|
|
}
|