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

LeTraceurSnorkLibrary / MessSaga / 23856747972

01 Apr 2026 03:31PM UTC coverage: 31.083% (-0.6%) from 31.674%
23856747972

Pull #15

github

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

24 of 80 new or added lines in 10 files covered. (30.0%)

3 existing lines in 3 files now uncovered.

442 of 1422 relevant lines covered (31.08%)

0.73 hits per line

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

0.0
/app/Services/ImportService.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Services;
6

7
use App\Models\MediaAttachment;
8
use App\Models\MessengerAccount;
9
use App\Services\Import\Archives\DTO\ArchiveExtractionResult;
10
use App\Services\Import\DTO\PreparedMessageRowResult;
11
use App\Services\Import\MessageInsertService;
12
use App\Services\Import\MessagePreparationService;
13
use App\Services\Import\Strategies\ImportStrategyInterface;
14
use App\Services\Media\Storage\MediaStorageInterface;
15
use App\Services\Parsers\ParserRegistry;
16
use Illuminate\Database\QueryException;
17
use Illuminate\Support\Facades\DB;
18
use Illuminate\Support\Facades\Log;
19
use InvalidArgumentException;
20
use RuntimeException;
21

22
class ImportService
23
{
24
    /**
25
     * @param ParserRegistry          $parserRegistry
26
     * @param MessagePreparationService $messagePreparationService
27
     * @param MessageInsertService      $messageInsertService
28
     * @param MediaStorageInterface     $mediaStorage
29
     */
30
    public function __construct(
31
        protected ParserRegistry          $parserRegistry,
32
        protected MessagePreparationService $messagePreparationService,
33
        protected MessageInsertService      $messageInsertService,
34
        protected MediaStorageInterface     $mediaStorage,
35
    ) {
36
    }
×
37

38
    /**
39
     * @param int                     $userId
40
     * @param string                  $messengerType
41
     * @param ImportStrategyInterface $strategy
42
     * @param ArchiveExtractionResult $extractedExportFile
43
     *
44
     * @throws QueryException
45
     * @return void
46
     */
47
    public function import(
48
        int                     $userId,
49
        string                  $messengerType,
50
        ImportStrategyInterface $strategy,
51
        ArchiveExtractionResult $extractedExportFile
52
    ): void {
53
        $exportFilePath = $extractedExportFile->getExportFileAbsolutePath();
×
54
        $mediaRootPath  = $extractedExportFile->getMediaRootPath();
×
55

56
        if ($exportFilePath === null) {
×
57
            return;
×
58
        }
59

60
        try {
61
            $parser               = $this->parserRegistry->get($messengerType);
×
62
            $importedConversation = $parser->parse($exportFilePath);
×
63
        } catch (RuntimeException|InvalidArgumentException $e) {
×
64
            Log::error('Import parsing failed', [
×
65
                'user_id'        => $userId,
×
66
                'messenger_type' => $messengerType,
×
67
                'path'           => $exportFilePath,
×
68
                'error'          => $e->getMessage(),
×
69
                'trace'          => $e->getTraceAsString(),
×
70
            ]);
×
71

72
            return;
×
73
        }
74

75
        if (!$importedConversation->hasConversation()) {
×
76
            Log::notice('Import skipped - no conversation data', [
×
77
                'user_id'        => $userId,
×
78
                'messenger_type' => $messengerType,
×
79
            ]);
×
80

81
            return;
×
82
        }
83

84
        $conversation = DB::transaction(function () use (
×
85
            $userId,
×
86
            $messengerType,
×
87
            $importedConversation,
×
88
            $strategy
×
89
        ) {
×
90
            $conversationData = $importedConversation->getConversationData();
×
91

92
            $account = MessengerAccount::firstOrCreate(
×
93
                [
×
94
                    'user_id' => $userId,
×
95
                    'type'    => $messengerType,
×
96
                ],
×
97
                [
×
98
                    'name' => $conversationData['account_name'] ?? ucfirst($messengerType),
×
99
                    'meta' => $conversationData['account_meta'] ?? [],
×
100
                ],
×
101
            );
×
102

103
            return $strategy->resolveConversation(
×
104
                account: $account,
×
105
                conversationData: $conversationData
×
106
            );
×
107
        });
×
108

109
        if (!$conversation) {
×
110
            Log::warning('Import aborted - no conversation target', [
×
111
                'mode'    => $strategy->getName(),
×
112
                'user_id' => $userId,
×
113
            ]);
×
114

115
            return;
×
116
        }
117

118
        $messagesRelation    = $parser->getMessagesRelation($conversation);
×
119
        $existingExternalIds = $messagesRelation
×
120
            ->whereNotNull('external_id')
×
121
            ->pluck('external_id')
×
122
            ->map(static fn($id): string => (string)$id)
×
123
            ->flip();
×
124
        $existingDedupHashes = $messagesRelation
×
125
            ->whereNotNull('dedup_hash')
×
126
            ->pluck('dedup_hash')
×
127
            ->map(static fn($hash): string => (string)$hash)
×
128
            ->flip();
×
129

130
        $preparedMessages  = [];
×
131
        $copiedMediaPaths  = [];
×
132
        $messageModelClass = $parser->getMessageModelClass();
×
133
        foreach ($importedConversation->getMessages() as $message) {
×
134
            $externalId = $this->messagePreparationService->normalizeExternalId($message['external_id'] ?? null);
×
135
            $dedupHash  = $this->messagePreparationService->buildDeduplicationHash($message);
×
136

137
            if ($externalId !== null && $existingExternalIds->has($externalId)) {
×
138
                continue;
×
139
            }
140
            if ($existingDedupHashes->has($dedupHash)) {
×
141
                continue;
×
142
            }
143

144
            if ($externalId !== null) {
×
145
                $existingExternalIds->put($externalId, true);
×
146
            }
147
            $existingDedupHashes->put($dedupHash, true);
×
148

149
            $attachmentStoredPath = $this->messagePreparationService->copyAttachmentForMessage(
×
150
                $mediaRootPath,
×
151
                $message,
×
152
                $conversation->id
×
153
            );
×
154
            if ($attachmentStoredPath !== null) {
×
155
                $copiedMediaPaths[$attachmentStoredPath] = true;
×
156
            }
157

158
            $message['dedup_hash'] = $dedupHash;
×
159

160
            $preparedMessages[] = $this->messagePreparationService->prepareMessageRowForInsert(
×
161
                $message,
×
162
                $conversation->id,
×
163
                $messageModelClass,
×
164
                $attachmentStoredPath,
×
165
            );
×
166
        }
167

168
        $importedCount = 0;
×
169
        try {
170
            DB::transaction(function () use (
×
171
                $messageModelClass,
×
172
                $preparedMessages,
×
173
                $conversation,
×
174
                &$importedCount,
×
175
                &$copiedMediaPaths
×
176
            ) {
×
177
                /**
178
                 * @var PreparedMessageRowResult $prepared
179
                 */
180
                foreach ($preparedMessages as $prepared) {
×
181
                    $msg = $this->messageInsertService->createMessageSafely($messageModelClass, $prepared->getRow());
×
182
                    if ($msg === null) {
×
183
                        $preparedMedia = $prepared->getMedia();
×
184
                        $preparedPath  = is_array($preparedMedia)
×
185
                            ? ($preparedMedia['stored_path'] ?? null)
×
186
                            : null;
×
NEW
187
                        if (is_string($preparedPath) && $preparedPath !== '' && $this->mediaStorage->exists($preparedPath)) {
×
NEW
188
                            $this->mediaStorage->delete($preparedPath);
×
UNCOV
189
                            unset($copiedMediaPaths[$preparedPath]);
×
190
                        }
191

192
                        continue;
×
193
                    }
194

195
                    $media = $prepared->getMedia();
×
196
                    if (isset($media)) {
×
197
                        $media = MediaAttachment::create(array_merge($media, [
×
198
                            'conversation_id' => $conversation->id,
×
199
                        ]));
×
200
                        $msg->update(['media_attachment_id' => $media->id]);
×
201
                    }
202

203
                    $importedCount++;
×
204
                }
205
            });
×
206
        } catch (QueryException $e) {
×
207
            foreach (array_keys($copiedMediaPaths) as $path) {
×
NEW
208
                if ($this->mediaStorage->exists($path)) {
×
NEW
209
                    $this->mediaStorage->delete($path);
×
210
                }
211
            }
212

213
            throw $e;
×
214
        }
215

216
        if ($importedCount > 0) {
×
217
            Log::info('Messages imported', [
×
218
                'conversation_id' => $conversation->id,
×
219
                'count'           => $importedCount,
×
220
            ]);
×
221
        } else {
222
            Log::info('No new messages to import', [
×
223
                'conversation_id' => $conversation->id,
×
224
            ]);
×
225
        }
226
    }
227
}
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