normalizedClientTempId($payload['client_temp_id'] ?? null); $created = false; /** @var Message $message */ $message = DB::transaction(function () use ($conversation, $sender, $payload, $body, $files, $clientTempId, &$created) { $existing = $this->findExistingMessage($conversation, $sender, $clientTempId); if ($existing) { return $existing; } $message = Message::query()->create([ 'conversation_id' => $conversation->id, 'sender_id' => $sender->id, 'client_temp_id' => $clientTempId, 'message_type' => empty($files) ? 'text' : ($body === '' ? 'attachment' : 'text'), 'body' => $body, 'reply_to_message_id' => $payload['reply_to_message_id'] ?? null, ]); foreach ($files as $file) { if ($file instanceof UploadedFile) { $this->storeAttachment($file, $message, $sender->id); } } $conversation->forceFill([ 'last_message_id' => $message->id, 'last_message_at' => $message->created_at, ])->save(); $created = true; return $message; }); if (! $created) { return $message->fresh(['sender:id,username,name', 'attachments', 'reactions']); } $participantIds = $this->conversationState->activeParticipantIds($conversation); $this->conversationState->touchConversationCachesForUsers($participantIds); DB::afterCommit(function () use ($conversation, $message, $sender, $participantIds): void { $this->notifications->notifyNewMessage($conversation, $message, $sender); $this->searchIndexer->indexMessage($message); event(new MessageCreated($conversation, $message, $sender->id)); foreach ($participantIds as $participantId) { event(new ConversationUpdated($participantId, $conversation, 'message.created')); } }); return $message->fresh(['sender:id,username,name', 'attachments', 'reactions']); } private function findExistingMessage(Conversation $conversation, User $sender, ?string $clientTempId): ?Message { if ($clientTempId === null) { return null; } return Message::query() ->where('conversation_id', $conversation->id) ->where('sender_id', $sender->id) ->where('client_temp_id', $clientTempId) ->whereNull('deleted_at') ->first(); } private function normalizedClientTempId(mixed $value): ?string { $normalized = trim((string) $value); return $normalized === '' ? null : $normalized; } private function storeAttachment(UploadedFile $file, Message $message, int $userId): void { $mime = (string) $file->getMimeType(); $finfo = finfo_open(FILEINFO_MIME_TYPE); $finfoMime = $finfo ? (string) finfo_file($finfo, $file->getPathname()) : ''; if ($finfo) { finfo_close($finfo); } $detectedMime = $finfoMime !== '' ? $finfoMime : $mime; $allowedImage = (array) config('messaging.attachments.allowed_image_mimes', []); $allowedFile = (array) config('messaging.attachments.allowed_file_mimes', []); $type = in_array($detectedMime, $allowedImage, true) ? 'image' : 'file'; $allowed = $type === 'image' ? $allowedImage : $allowedFile; abort_unless(in_array($detectedMime, $allowed, true), 422, 'Unsupported attachment type.'); $maxBytes = $type === 'image' ? ((int) config('messaging.attachments.max_image_kb', 10240) * 1024) : ((int) config('messaging.attachments.max_file_kb', 25600) * 1024); abort_if($file->getSize() > $maxBytes, 422, 'Attachment exceeds allowed size.'); $year = now()->format('Y'); $month = now()->format('m'); $ext = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin'); $path = "messages/{$message->conversation_id}/{$year}/{$month}/" . uniqid('att_', true) . ".{$ext}"; $diskName = (string) config('messaging.attachments.disk', 'local'); Storage::disk($diskName)->put($path, file_get_contents($file->getPathname())); $width = null; $height = null; if ($type === 'image') { $dimensions = @getimagesize($file->getPathname()); $width = isset($dimensions[0]) ? (int) $dimensions[0] : null; $height = isset($dimensions[1]) ? (int) $dimensions[1] : null; } MessageAttachment::query()->create([ 'message_id' => $message->id, 'disk' => $diskName, 'user_id' => $userId, 'type' => $type, 'mime' => $detectedMime, 'size_bytes' => (int) $file->getSize(), 'width' => $width, 'height' => $height, 'sha256' => hash_file('sha256', $file->getPathname()), 'original_name' => substr((string) $file->getClientOriginalName(), 0, 255), 'storage_path' => $path, 'created_at' => now(), ]); } }