Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\CardDav;
|
||||
|
||||
use App\Contracts\CardDav\CardDavClientInterface;
|
||||
use App\Contracts\PhoneNumberNormalizerInterface;
|
||||
use App\Models\Contact;
|
||||
use App\Models\ContactGroup;
|
||||
use App\Models\SourceSyncRun;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class CardDavContactSyncService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CardDavClientInterface $client,
|
||||
private readonly VCardParser $parser,
|
||||
private readonly PhoneNumberNormalizerInterface $phoneNumberNormalizer,
|
||||
) {}
|
||||
|
||||
public function sync(): SourceSyncRun
|
||||
{
|
||||
$run = SourceSyncRun::query()->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<int, int> $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<int, string> $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<int, string> $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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user