This commit is contained in:
199
app/Http/Controllers/Shop/ChatController.php
Normal file
199
app/Http/Controllers/Shop/ChatController.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user