Skip to content

Commit

Permalink
feat: implement DAV client subscriptions (#6751)
Browse files Browse the repository at this point in the history
  • Loading branch information
asbiin committed Aug 28, 2023
1 parent 67a3acf commit 2286e79
Show file tree
Hide file tree
Showing 85 changed files with 5,620 additions and 104 deletions.
36 changes: 36 additions & 0 deletions app/Console/Commands/Local/UpdateAddressBookSubscription.php
@@ -0,0 +1,36 @@
<?php

namespace App\Console\Commands\Local;

use App\Domains\Contact\DavClient\Jobs\SynchronizeAddressBooks;
use App\Models\AddressBookSubscription;
use Illuminate\Console\Command;

class UpdateAddressBookSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:updateaddressbooksubscription
{--subscriptionId= : Id of the subscription to synchronize}
{--force : Force sync}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Update a subscription';

/**
* Execute the console command.
*/
public function handle()
{
$subscription = AddressBookSubscription::findOrFail($this->option('subscriptionId'));

SynchronizeAddressBooks::dispatch($subscription, $this->option('force'))->onQueue('high');
}
}
128 changes: 128 additions & 0 deletions app/Console/Commands/NewAddressBookSubscription.php
@@ -0,0 +1,128 @@
<?php

namespace App\Console\Commands;

use App\Domains\Contact\DavClient\Jobs\SynchronizeAddressBooks;
use App\Domains\Contact\DavClient\Services\CreateAddressBookSubscription;
use App\Models\AddressBookSubscription;
use App\Models\User;
use App\Models\Vault;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class NewAddressBookSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monica:newaddressbooksubscription
{--email= : Monica account to add subscription to}
{--vaultId= : Id of the vault to add subscription to}
{--url= : CardDAV url of the address book}
{--login= : Login}
{--password= : Password of the account}
{--pushonly : Set only push way}
{--getonly : Set only get way}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new dav subscription';

/**
* Execute the console command.
*/
public function handle(): int
{
if (($user = $this->user()) === null) {
return 1;
}
if (($vault = $this->vault()) === null) {
return 2;
}

if ($user->account_id !== $vault->account_id) {
$this->error('Vault does not belong to this account');

return 3;
}

$pushonly = $this->option('pushonly');
$getonly = $this->option('getonly');
if ($pushonly && $getonly) {
$this->error('Cannot set both pushonly and getonly');

return 4;
}

$url = $this->option('url') ?? $this->ask('CardDAV url of the address book');
$login = $this->option('login') ?? $this->ask('Login name');
$password = $this->option('password') ?? $this->secret('User password');

try {
$subscription = app(CreateAddressBookSubscription::class)->execute([
'account_id' => $user->account_id,
'vault_id' => $vault->id,
'author_id' => $user->id,
'base_uri' => $url,
'username' => $login,
'password' => $password,
]);

if ($pushonly) {
$subscription->sync_way = AddressBookSubscription::WAY_PUSH;
} elseif ($getonly) {
$subscription->sync_way = AddressBookSubscription::WAY_GET;
}
$subscription->save();
} catch (\Exception $e) {
$this->error('Could not add subscription');
$this->error($e->getMessage());

return -1;
}

$this->info("Subscription added: {$subscription->id}");
SynchronizeAddressBooks::dispatch($subscription, true)->onQueue('high');

return 0;
}

private function user(): ?User
{
if (($email = $this->option('email')) === null) {
$this->error('Please provide an email address');

return null;
}

try {
return User::where('email', $email)->firstOrFail();
} catch (ModelNotFoundException) {
$this->error('Could not find user');

return null;
}
}

private function vault(): ?Vault
{
if (($vaultId = $this->option('vaultId')) === null) {
$this->error('Please provide an vaultId');

return null;
}

try {
return Vault::findOrFail($vaultId);
} catch (ModelNotFoundException) {
$this->error('Could not find vault');

return null;
}
}
}
2 changes: 2 additions & 0 deletions app/Console/Kernel.php
Expand Up @@ -4,6 +4,7 @@

use App\Console\Scheduling\CronEvent;
use App\Domains\Contact\Dav\Jobs\CleanSyncToken;
use App\Domains\Contact\DavClient\Jobs\UpdateAddressBooks;
use App\Domains\Contact\ManageReminders\Jobs\ProcessScheduledContactReminders;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
Expand Down Expand Up @@ -40,6 +41,7 @@ protected function schedule(Schedule $schedule)
if (config('telescope.enabled')) {
$this->scheduleCommand($schedule, 'telescope:prune', 'daily');
}
$this->scheduleJob($schedule, UpdateAddressBooks::class, 'hourly');
$this->scheduleJob($schedule, ProcessScheduledContactReminders::class, 'minutes', 1);
$this->scheduleJob($schedule, CleanSyncToken::class, 'daily');
}
Expand Down
Expand Up @@ -485,12 +485,12 @@ public function deleteCard($addressBookId, $cardUri): bool

return true;
} elseif ($obj !== null && $obj instanceof Group) {
(new DestroyGroup)->execute([
DestroyGroup::dispatch([
'account_id' => $this->user->account_id,
'author_id' => $this->user->id,
'vault_id' => $obj->vault_id,
'group_id' => $obj->id,
]);
])->onQueue('high');

return true;
}
Expand Down
48 changes: 48 additions & 0 deletions app/Domains/Contact/DavClient/Jobs/DeleteMultipleVCard.php
@@ -0,0 +1,48 @@
<?php

namespace App\Domains\Contact\DavClient\Jobs;

use App\Models\AddressBookSubscription;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class DeleteMultipleVCard implements ShouldQueue
{
use Batchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Create a new job instance.
*/
public function __construct(
private AddressBookSubscription $subscription,
private array $hrefs
) {
$this->subscription = $subscription->withoutRelations();
}

/**
* Update the Last Consulted At field for the given contact.
*/
public function handle(): void
{
if (! $this->batching()) {
return; // @codeCoverageIgnore
}

$jobs = collect($this->hrefs)
->map(fn (string $href): DeleteVCard => $this->deleteVCard($href));

$this->batch()->add($jobs);
}

/**
* Delete the contact.
*/
private function deleteVCard(string $href): DeleteVCard
{
return new DeleteVCard($this->subscription, $href);
}
}
34 changes: 34 additions & 0 deletions app/Domains/Contact/DavClient/Jobs/DeleteVCard.php
@@ -0,0 +1,34 @@
<?php

namespace App\Domains\Contact\DavClient\Jobs;

use App\Models\AddressBookSubscription;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class DeleteVCard implements ShouldQueue
{
use Batchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Create a new job instance.
*/
public function __construct(
private AddressBookSubscription $subscription,
private string $uri
) {
$this->subscription = $subscription->withoutRelations();
}

/**
* Send Delete contact.
*/
public function handle(): void
{
$this->subscription->getClient()
->request('DELETE', $this->uri);
}
}
96 changes: 96 additions & 0 deletions app/Domains/Contact/DavClient/Jobs/GetMultipleVCard.php
@@ -0,0 +1,96 @@
<?php

namespace App\Domains\Contact\DavClient\Jobs;

use App\Domains\Contact\Dav\Jobs\UpdateVCard;
use App\Models\AddressBookSubscription;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Sabre\CardDAV\Plugin as CardDav;

class GetMultipleVCard implements ShouldQueue
{
use Batchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Create a new job instance.
*/
public function __construct(
private AddressBookSubscription $subscription,
private array $hrefs
) {
$this->subscription = $subscription->withoutRelations();
}

/**
* Update the Last Consulted At field for the given contact.
*/
public function handle(): void
{
if (! $this->batching()) {
return; // @codeCoverageIgnore
}

$data = $this->addressbookMultiget();

$jobs = collect($data)
->filter(fn (array $contact): bool => is_array($contact) && $contact['status'] === '200')
->map(fn (array $contact, string $href): ?UpdateVCard => $this->updateVCard($contact, $href))
->filter();

$this->batch()->add($jobs);
}

/**
* Update the contact.
*/
private function updateVCard(array $contact, string $href): ?UpdateVCard
{
$card = Arr::get($contact, 'properties.200.{'.CardDav::NS_CARDDAV.'}address-data');

return $card === null
? null
: new UpdateVCard([
'account_id' => $this->subscription->vault->account_id,
'author_id' => $this->subscription->user_id,
'vault_id' => $this->subscription->vault_id,
'uri' => $href,
'etag' => Arr::get($contact, 'properties.200.{DAV:}getetag'),
'card' => $card,
'external' => true,
]);
}

/**
* Get addressbook data.
*/
private function addressbookMultiget(): array
{
return $this->subscription->getClient()
->addressbookMultiget([
'{DAV:}getetag',
$this->getAddressDataProperty(),
], $this->hrefs);
}

/**
* Get data for address-data property.
*/
private function getAddressDataProperty(): array
{
$addressDataAttributes = Arr::get($this->subscription->capabilities, 'addressData', [
'content-type' => 'text/vcard',
'version' => '4.0',
]);

return [
'name' => '{'.CardDav::NS_CARDDAV.'}address-data',
'value' => null,
'attributes' => $addressDataAttributes,
];
}
}

0 comments on commit 2286e79

Please sign in to comment.