• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LeTraceurSnorkLibrary / MessSaga / 23844646769

01 Apr 2026 10:42AM UTC coverage: 31.674% (+31.1%) from 0.549%
23844646769

push

github

web-flow
feat: media files import

431 of 801 new or added lines in 30 files covered. (53.81%)

4 existing lines in 4 files now uncovered.

439 of 1386 relevant lines covered (31.67%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/app/Http/Controllers/Api/ConversationController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers\Api;
6

7
use App\Http\Controllers\Controller;
8
use App\Jobs\ProcessConversationMediaUpload;
9
use App\Models\Conversation;
10
use App\Models\MediaAttachment;
11
use App\Services\Parsers\ParserRegistry;
12
use App\Support\FilenameSanitizer;
13
use Illuminate\Database\Eloquent\Builder;
14
use Illuminate\Http\JsonResponse;
15
use Illuminate\Http\Request;
16
use Illuminate\Http\Response;
17
use Illuminate\Support\Facades\File;
18
use Illuminate\Support\Facades\Storage;
19
use Symfony\Component\HttpFoundation\StreamedResponse;
20
use Teapot\StatusCode\Http;
21

22
class ConversationController extends Controller
23
{
24
    /**
25
     * @param ParserRegistry $parserRegistry
26
     */
27
    public function __construct(
28
        private readonly ParserRegistry $parserRegistry
29
    ) {
30
    }
×
31

32
    /**
33
     * @param Request $request
34
     *
35
     * @return JsonResponse
36
     */
37
    public function index(Request $request): JsonResponse
38
    {
39
        $messengerType = $request->string('messenger')->toString();
×
40

41
        $query = Conversation::query()
×
42
            ->whereHas('messengerAccount', function (Builder $q) use ($request, $messengerType) {
×
43
                $q->where('user_id', $request->user()->id);
×
44

45
                if ($messengerType) {
×
46
                    $q->where('type', $messengerType);
×
47
                }
48
            })
×
49
            ->with('messengerAccount');
×
50

51
        $conversations = $query
×
52
            ->orderByDesc('id')
×
53
            ->get()
×
54
            ->map(function (Conversation $conversation) {
×
55
                $messengerType = $conversation->messengerAccount->type;
×
56

57
                /**
58
                 * Получаем парсер из реестра
59
                 */
60
                $parser = $this->parserRegistry->get($messengerType);
×
61

62
                /**
63
                 * Используем метод парсера для получения последнего сообщения
64
                 */
65
                $lastMessage = $parser->getMessagesRelation($conversation)
×
66
                    ->latest('sent_at')
×
67
                    ->first();
×
68

69
                return [
×
70
                    'id'      => $conversation->id,
×
71
                    'title'   => $conversation->title,
×
72
                    'preview' => $lastMessage?->text,
×
73
                    'type'    => $messengerType,
×
74
                ];
×
75
            });
×
76

77
        return response()->json($conversations);
×
78
    }
79

80
    /**
81
     * @param Request      $request
82
     * @param Conversation $conversation
83
     *
84
     * @return JsonResponse
85
     */
86
    public function messages(Request $request, Conversation $conversation): JsonResponse
87
    {
88
        abort_unless($conversation->messengerAccount->user_id === $request->user()->id, Http::FORBIDDEN);
×
89

90
        $messengerType = $conversation->messengerAccount->type;
×
91

92
        /**
93
         * Получаем парсер из реестра
94
         */
95
        $parser = $this->parserRegistry->get($messengerType);
×
96

97
        /**
98
         * Получаем сообщения через парсер
99
         */
100
        $messages = $parser->getMessagesRelation($conversation)
×
NEW
101
            ->with('mediaAttachment')
×
102
            ->orderBy('sent_at')
×
NEW
103
            ->get(['id', 'sender_name', 'sent_at', 'text', 'message_type', 'media_attachment_id']);
×
104

NEW
105
        $messages = $messages->map(function ($msg) use ($conversation) {
×
NEW
106
            $item = $msg->toArray();
×
107
            /**
108
             * @var MediaAttachment|null $media
109
             */
NEW
110
            $media         = $msg->mediaAttachment;
×
NEW
111
            $item['media'] = $media?->toApiArray($conversation->id, $msg->id);
×
112

NEW
113
            $mediaStoredPath               = $media?->stored_path ?? '';
×
NEW
114
            $hasStoredFile                 = $mediaStoredPath !== '';
×
NEW
115
            $item['is_media_without_file'] = !empty($media) && !$hasStoredFile;
×
116

NEW
117
            return $item;
×
NEW
118
        });
×
119

NEW
120
        $messagesHash = md5(
×
NEW
121
            json_encode(
×
NEW
122
                $messages,
×
NEW
123
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
×
NEW
124
            )
×
NEW
125
                ?: ''
×
NEW
126
        );
×
127

NEW
128
        return response()
×
NEW
129
            ->json($messages)
×
NEW
130
            ->header('X-Messages-Hash', $messagesHash);
×
131
    }
132

133
    /**
134
     * @param Request      $request
135
     * @param Conversation $conversation
136
     *
137
     * @return Response
138
     */
139
    public function destroy(Request $request, Conversation $conversation): Response
140
    {
141
        abort_unless($conversation->messengerAccount->user_id === $request->user()->id, Http::FORBIDDEN);
×
142

143
        /**
144
         * Каскадно удалит сообщения за счёт foreign key
145
         */
146
        $conversation->delete();
×
147

148
        return response()->noContent();
×
149
    }
150

151
    /**
152
     * Отдаёт файл вложения сообщения (медиа). Проверяет доступ пользователя к переписке.
153
     *
154
     * @param Request      $request
155
     * @param Conversation $conversation
156
     * @param int          $messageId
157
     *
158
     * @return Response|StreamedResponse
159
     */
160
    public function attachment(Request $request, Conversation $conversation, int $messageId): Response|StreamedResponse
161
    {
NEW
162
        abort_unless($conversation->messengerAccount->user_id === $request->user()->id, Http::FORBIDDEN);
×
163

NEW
164
        $parser  = $this->parserRegistry->get($conversation->messengerAccount->type);
×
NEW
165
        $message = $parser->getMessagesRelation($conversation)
×
NEW
166
            ->with('mediaAttachment')
×
NEW
167
            ->find($messageId);
×
168

NEW
169
        $storedPath = $message?->mediaAttachment?->stored_path;
×
NEW
170
        if (!$message || $storedPath === null || $storedPath === '') {
×
NEW
171
            abort(Http::NOT_FOUND);
×
172
        }
173

NEW
174
        if (!Storage::exists($storedPath)) {
×
NEW
175
            abort(Http::NOT_FOUND);
×
176
        }
177

NEW
178
        $mime     = File::mimeType(Storage::path($storedPath));
×
NEW
179
        $mime     = $mime
×
NEW
180
            ?: 'application/octet-stream';
×
NEW
181
        $filename = FilenameSanitizer::sanitize(basename($storedPath));
×
182

NEW
183
        return response()->streamDownload(
×
NEW
184
            function () use ($storedPath) {
×
NEW
185
                $stream = Storage::readStream($storedPath);
×
NEW
186
                if (is_resource($stream)) {
×
NEW
187
                    fpassthru($stream);
×
NEW
188
                    fclose($stream);
×
189
                }
NEW
190
            },
×
NEW
191
            $filename,
×
NEW
192
            [
×
NEW
193
                'Content-Type'        => $mime,
×
NEW
194
                'Content-Disposition' => 'inline; filename="' . addslashes($filename) . '"',
×
NEW
195
            ]
×
NEW
196
        );
×
197
    }
198

199
    /**
200
     * Догрузка медиа в существующую переписку: загрузка архива, сопоставление по export_path в media_attachments.
201
     *
202
     * @param Request      $request
203
     * @param Conversation $conversation
204
     *
205
     * @return JsonResponse
206
     */
207
    public function uploadMedia(Request $request, Conversation $conversation): JsonResponse
208
    {
NEW
209
        abort_unless($conversation->messengerAccount->user_id === $request->user()->id, Http::FORBIDDEN);
×
210

NEW
211
        $request->validate([
×
NEW
212
            'file' => 'required|file|mimes:zip|max:262144',
×
NEW
213
        ]);
×
214

NEW
215
        $path = $request->file('file')->store('chat_imports');
×
216

NEW
217
        ProcessConversationMediaUpload::dispatch(
×
NEW
218
            userId: $request->user()->id,
×
NEW
219
            conversationId: $conversation->id,
×
NEW
220
            path: $path,
×
NEW
221
        );
×
222

NEW
223
        return response()->json([
×
NEW
224
            'status'  => 'queued',
×
NEW
225
            'message' => 'Догрузка медиа поставлена в очередь',
×
NEW
226
        ]);
×
227
    }
228
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc