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

LeTraceurSnorkLibrary / MessSaga / 24455793340

15 Apr 2026 12:57PM UTC coverage: 39.248% (+2.7%) from 36.562%
24455793340

Pull #20

github

web-flow
Merge 4e3783ced into 4ac78237e
Pull Request #20: feat: tariffs

161 of 280 new or added lines in 14 files covered. (57.5%)

2 existing lines in 2 files now uncovered.

741 of 1888 relevant lines covered (39.25%)

0.78 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\Services\Quota\UserMediaQuotaService;
14
use App\Support\FilenameSanitizer;
15
use Illuminate\Database\Eloquent\Builder;
16
use Illuminate\Http\JsonResponse;
17
use Illuminate\Http\Request;
18
use Illuminate\Http\Response;
19
use Symfony\Component\HttpFoundation\StreamedResponse;
20
use Teapot\StatusCode\Http;
21

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

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

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

49
                if ($messengerType) {
×
50
                    $q->where('type', $messengerType);
×
51
                }
52
            })
×
53
            ->with('messengerAccount');
×
54

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

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

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

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

81
        return response()->json($conversations);
×
82
    }
83

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

94
        $messengerType = $conversation->messengerAccount->type;
×
95

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

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

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

117
            $mediaStoredPath               = $media?->stored_path ?? '';
×
118
            $hasStoredFile                 = $mediaStoredPath !== '';
×
119
            $item['is_media_without_file'] = !empty($media) && !$hasStoredFile;
×
120

121
            return $item;
×
122
        });
×
123

124
        $messagesHash = md5(
×
125
            json_encode(
×
126
                $messages,
×
127
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
×
128
            )
×
129
                ?: ''
×
130
        );
×
131

132
        return response()
×
133
            ->json($messages)
×
134
            ->header('X-Messages-Hash', $messagesHash);
×
135
    }
136

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

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

152
        return response()->noContent();
×
153
    }
154

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

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

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

178
        if (!$this->mediaStorage->exists($storedPath)) {
×
179
            abort(Http::NOT_FOUND);
×
180
        }
181

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

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

206
    /**
207
     * Догрузка медиа в существующую переписку: загрузка архива, сопоставление по export_path в media_attachments.
208
     *
209
     * @param Request      $request
210
     * @param Conversation $conversation
211
     *
212
     * @return JsonResponse
213
     */
214
    public function uploadMedia(Request $request, Conversation $conversation): JsonResponse
215
    {
216
        abort_unless($conversation->messengerAccount->user_id === $request->user()->id, Http::FORBIDDEN);
×
NEW
217
        $user   = $request->user();
×
NEW
218
        $quota  = $this->userMediaQuotaService->snapshot($user);
×
NEW
219
        $reason = $quota->getMediaUploadBlockReason();
×
NEW
220
        if ($reason !== null) {
×
NEW
221
            return response()->json([
×
NEW
222
                'status'  => 'rejected',
×
NEW
223
                'message' => 'Загрузка медиа недоступна.',
×
NEW
224
                'reason'  => $reason,
×
NEW
225
                'quota'   => $quota->toArray(),
×
NEW
226
            ], Http::PAYMENT_REQUIRED);
×
227
        }
228

229
        $request->validate([
×
230
            'file' => 'required|file|mimes:zip|max:262144',
×
231
        ]);
×
232

233
        $importsTmpDisk = (string)config('filesystems.imports_tmp_disk', 'imports_tmp');
×
234
        $path           = $request->file('file')->store('chat_imports', $importsTmpDisk);
×
235

236
        ProcessConversationMediaUpload::dispatch(
×
NEW
237
            userId: $user->id,
×
238
            conversationId: $conversation->id,
×
239
            path: $path,
×
240
        );
×
241

242
        return response()->json([
×
243
            'status'  => 'queued',
×
244
            'message' => 'Догрузка медиа поставлена в очередь',
×
245
        ]);
×
246
    }
247
}
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