create([ 'source' => 'carddav', 'status' => 'running', 'started_at' => Carbon::now(), ]); try { $cards = $this->client->fetchCards(); DB::transaction(function () use ($run, $cards): void { $seenContactExternalIds = []; $seenGroupExternalIds = []; $syncedContacts = 0; foreach ($cards as $card) { $parsedCard = $this->parser->parse($card->vcard); $externalId = $this->externalId($parsedCard, $card); $seenContactExternalIds[] = $externalId; $contact = $this->upsertContact($externalId, $card, $parsedCard); $this->replacePhoneNumbers($contact, $parsedCard); $groupIds = []; foreach ($parsedCard->categories as $category) { $group = $this->upsertGroup($category); $groupIds[] = $group->id; $seenGroupExternalIds[] = (string) $group->external_id; } $this->syncCardDavGroupMemberships($contact, $groupIds); $syncedContacts++; } $deletedContacts = $this->deleteMissingContacts($seenContactExternalIds); $this->deleteMissingGroups(array_values(array_unique($seenGroupExternalIds))); $run->update([ 'status' => 'success', 'finished_at' => Carbon::now(), 'fetched_contacts' => count($cards), 'synced_contacts' => $syncedContacts, 'deleted_contacts' => $deletedContacts, 'message' => $syncedContacts.' contact(s) CardDAV synchronise(s).', ]); }); } catch (Throwable $exception) { $run->update([ 'status' => 'failed', 'finished_at' => Carbon::now(), 'error' => $exception->getMessage(), ]); throw $exception; } return $run->refresh(); } private function upsertContact(string $externalId, CardDavRemoteCard $card, ParsedVCard $parsedCard): Contact { $contact = Contact::withTrashed() ->where('source', 'carddav') ->where('external_id', $externalId) ->first(); $attributes = [ 'source' => 'carddav', 'external_id' => $externalId, 'remote_url' => $card->href, 'remote_etag' => $card->etag, 'first_name' => $parsedCard->firstName, 'last_name' => $this->lastName($parsedCard), 'organization' => $parsedCard->organization, 'email' => $parsedCard->email, 'synced_at' => Carbon::now(), ]; if (! $contact) { return Contact::query()->create($attributes); } $contact->fill($attributes); $contact->deleted_at = null; $contact->save(); return $contact; } private function replacePhoneNumbers(Contact $contact, ParsedVCard $parsedCard): void { $contact->phoneNumbers()->delete(); $defaultCountry = (string) config('relais_sms.default_settings.default_country'); foreach ($parsedCard->phoneNumbers as $index => $phoneNumber) { $result = $this->phoneNumberNormalizer->normalize($phoneNumber->rawNumber, $defaultCountry); $contact->phoneNumbers()->create([ 'label' => $phoneNumber->label, 'raw_number' => $result->rawNumber, 'normalized_number' => $result->normalizedNumber, 'country' => $result->country, 'is_valid' => $result->isValid, 'is_mobile' => $result->isMobile, 'is_primary' => $index === 0, 'validation_error' => $result->error, ]); } } private function upsertGroup(string $category): ContactGroup { $externalId = Str::slug($category); $slug = 'carddav-'.$externalId; $group = ContactGroup::withTrashed() ->where('source', 'carddav') ->where('external_id', $externalId) ->first(); $attributes = [ 'source' => 'carddav', 'external_id' => $externalId, 'name' => $category, 'slug' => $slug, 'synced_at' => Carbon::now(), ]; if (! $group) { return ContactGroup::query()->create($attributes); } $group->fill($attributes); $group->deleted_at = null; $group->save(); return $group; } /** * @param array $groupIds */ private function syncCardDavGroupMemberships(Contact $contact, array $groupIds): void { $currentCardDavGroupIds = $contact->groups() ->where('contact_groups.source', 'carddav') ->pluck('contact_groups.id') ->all(); if ($currentCardDavGroupIds !== []) { $contact->groups()->detach($currentCardDavGroupIds); } if ($groupIds !== []) { $contact->groups()->syncWithoutDetaching($groupIds); } } /** * @param array $seenExternalIds */ private function deleteMissingContacts(array $seenExternalIds): int { $contacts = Contact::query() ->where('source', 'carddav') ->whereNotIn('external_id', $seenExternalIds ?: ['']) ->get(); foreach ($contacts as $contact) { $cardDavGroupIds = $contact->groups() ->where('contact_groups.source', 'carddav') ->pluck('contact_groups.id') ->all(); if ($cardDavGroupIds !== []) { $contact->groups()->detach($cardDavGroupIds); } $contact->delete(); } return $contacts->count(); } /** * @param array $seenExternalIds */ private function deleteMissingGroups(array $seenExternalIds): void { ContactGroup::query() ->where('source', 'carddav') ->whereNotIn('external_id', $seenExternalIds ?: ['']) ->delete(); } private function externalId(ParsedVCard $parsedCard, CardDavRemoteCard $card): string { return $parsedCard->uid ?: sha1($card->href); } private function lastName(ParsedVCard $parsedCard): string { if ($parsedCard->lastName) { return $parsedCard->lastName; } if ($parsedCard->fullName) { return $parsedCard->fullName; } if ($parsedCard->organization) { return $parsedCard->organization; } return 'Contact CardDAV'; } }