Files
tehnobox/app/Http/Controllers/Shop/ChatController.php
ssww23 93a655235a
Some checks failed
Deploy / deploy (push) Has been cancelled
Initial commit
2026-03-10 00:55:37 +03:00

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;
}
}