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

LeTraceurSnorkLibrary / MessSaga / 23899406397

02 Apr 2026 12:01PM UTC coverage: 30.548% (-1.1%) from 31.674%
23899406397

Pull #15

github

web-flow
Merge 7f16eb62e into a7d6f3637
Pull Request #15: feat: add s3 as storage

39 of 155 new or added lines in 14 files covered. (25.16%)

4 existing lines in 4 files now uncovered.

457 of 1496 relevant lines covered (30.55%)

0.71 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\Media\Storage\MediaStorageInterface;
12
use App\Services\Parsers\ParserRegistry;
13
use App\Support\FilenameSanitizer;
14
use Illuminate\Database\Eloquent\Builder;
15
use Illuminate\Http\JsonResponse;
16
use Illuminate\Http\Request;
17
use Illuminate\Http\Response;
18
use Symfony\Component\HttpFoundation\StreamedResponse;
19
use Teapot\StatusCode\Http;
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

118
            return $item;
×
119
        });
×
120

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

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

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

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

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

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

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

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

NEW
175
        if (!$this->mediaStorage->exists($storedPath)) {
×
176
            abort(Http::NOT_FOUND);
×
177
        }
178

NEW
179
        $mime = $message?->mediaAttachment?->mime_type;
×
NEW
180
        if (!is_string($mime) || $mime === '') {
×
NEW
181
            $mime = $this->mediaStorage->mimeType($storedPath);
×
182
        }
183
        $mime     = $mime
×
184
            ?: 'application/octet-stream';
×
185
        $filename = FilenameSanitizer::sanitize(basename($storedPath));
×
186

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

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

215
        $request->validate([
×
216
            'file' => 'required|file|mimes:zip|max:262144',
×
217
        ]);
×
218

NEW
219
        $importsTmpDisk = (string)config('filesystems.imports_tmp_disk', 'imports_tmp');
×
NEW
220
        $path           = $request->file('file')->store('chat_imports', $importsTmpDisk);
×
221

222
        ProcessConversationMediaUpload::dispatch(
×
223
            userId: $request->user()->id,
×
224
            conversationId: $conversation->id,
×
225
            path: $path,
×
226
        );
×
227

228
        return response()->json([
×
229
            'status'  => 'queued',
×
230
            'message' => 'Догрузка медиа поставлена в очередь',
×
231
        ]);
×
232
    }
233
}
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