Files
RelaisSMS/app/Services/Calendar/CalendarEventSyncService.php
T

215 lines
7.3 KiB
PHP

<?php
namespace App\Services\Calendar;
use App\Contracts\Calendar\CalendarClientInterface;
use App\Models\CalendarEvent;
use App\Models\CalendarReminder;
use App\Models\SourceSyncRun;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Throwable;
class CalendarEventSyncService
{
public function __construct(
private readonly CalendarClientInterface $client,
private readonly ICalendarParser $parser,
) {}
public function sync(?Carbon $from = null, ?Carbon $to = null): SourceSyncRun
{
$from ??= Carbon::now()->subDay();
$to ??= Carbon::now()->addDays((int) config('relais_sms.caldav.lookahead_days', 90));
$run = SourceSyncRun::query()->create([
'source' => 'caldav_calendar',
'status' => 'running',
'started_at' => Carbon::now(),
]);
try {
$remoteEvents = $this->client->fetchEvents($from, $to);
DB::transaction(function () use ($run, $remoteEvents, $from, $to): void {
$seenExternalIds = [];
$syncedEvents = 0;
$scheduledReminders = 0;
foreach ($remoteEvents as $remoteEvent) {
foreach ($this->parser->parse($remoteEvent->ical, $from, $to) as $parsedEvent) {
$externalId = $parsedEvent->recurrenceId === null
? $parsedEvent->uid
: 'recurrence:'.sha1($parsedEvent->uid.'|'.$parsedEvent->recurrenceId->format('Y-m-d H:i:s'));
$seenExternalIds[] = $externalId;
$event = $this->upsertEvent($externalId, $remoteEvent, $parsedEvent);
$scheduledReminders += $this->syncReminders($event, $parsedEvent);
$syncedEvents++;
}
}
$cancelledEvents = $this->cancelMissingEvents($seenExternalIds);
$run->update([
'status' => 'success',
'finished_at' => Carbon::now(),
'fetched_contacts' => count($remoteEvents),
'synced_contacts' => $syncedEvents,
'deleted_contacts' => $cancelledEvents,
'message' => $syncedEvents.' evenement(s) CalDAV synchronise(s), '.$scheduledReminders.' rappel(s) programme(s).',
]);
});
} catch (Throwable $exception) {
$run->update([
'status' => 'failed',
'finished_at' => Carbon::now(),
'error' => $exception->getMessage(),
]);
throw $exception;
}
return $run->refresh();
}
private function upsertEvent(string $externalId, CalendarRemoteEvent $remoteEvent, ParsedCalendarEvent $parsedEvent): CalendarEvent
{
$event = CalendarEvent::query()
->where('source', 'caldav')
->where('external_id', $externalId)
->first();
// Preserve the reminder history when an existing single event becomes a series.
if (! $event && $parsedEvent->recurrenceId !== null) {
$event = CalendarEvent::query()
->where('source', 'caldav')
->where('external_id', $parsedEvent->uid)
->where('starts_at', $parsedEvent->recurrenceId->format('Y-m-d H:i:s'))
->first();
}
$attributes = [
'source' => 'caldav',
'external_id' => $externalId,
'remote_url' => $remoteEvent->href,
'remote_etag' => $remoteEvent->etag,
'summary' => $parsedEvent->summary,
'description' => $parsedEvent->description,
'location' => $parsedEvent->location,
'starts_at' => $parsedEvent->startsAt,
'ends_at' => $parsedEvent->endsAt,
'attendees' => $parsedEvent->attendees,
'synced_at' => Carbon::now(),
'deleted_at' => null,
];
if (! $event) {
return CalendarEvent::query()->create($attributes);
}
$event->fill($attributes);
$event->save();
return $event;
}
private function syncReminders(CalendarEvent $event, ParsedCalendarEvent $parsedEvent): int
{
$seenReminderKeys = [];
$scheduledReminders = 0;
foreach ($parsedEvent->alarms as $index => $alarm) {
if ($alarm->action !== 'DISPLAY') {
continue;
}
$remindAt = $this->remindAt($parsedEvent, $alarm);
if ($remindAt === null) {
continue;
}
$reminderKey = sha1($alarm->action.'|'.$alarm->trigger.'|'.$index);
$seenReminderKeys[] = $reminderKey;
$reminder = CalendarReminder::query()
->where('calendar_event_id', $event->id)
->where('reminder_key', $reminderKey)
->first();
$attributes = [
'action' => $alarm->action,
'trigger_minutes' => $alarm->triggerMinutes,
'remind_at' => $remindAt,
];
if (! $reminder) {
CalendarReminder::query()->create(array_merge($attributes, [
'calendar_event_id' => $event->id,
'reminder_key' => $reminderKey,
'status' => 'pending',
]));
$scheduledReminders++;
continue;
}
if (! in_array($reminder->status, ['sent', 'sending', 'uncertain', 'skipped'], true)) {
$reminder->update(array_merge($attributes, [
'status' => 'pending',
'last_error' => null,
]));
$scheduledReminders++;
}
}
CalendarReminder::query()
->where('calendar_event_id', $event->id)
->whereNotIn('reminder_key', $seenReminderKeys ?: [''])
->whereIn('status', ['pending', 'failed'])
->update([
'status' => 'cancelled',
'last_error' => 'Rappel absent de la derniere synchronisation.',
]);
return $scheduledReminders;
}
private function remindAt(ParsedCalendarEvent $event, ParsedCalendarAlarm $alarm): ?Carbon
{
if ($alarm->absoluteRemindAt !== null) {
return $alarm->absoluteRemindAt;
}
if ($event->startsAt === null || $alarm->triggerMinutes === null) {
return null;
}
return $event->startsAt->copy()->addMinutes($alarm->triggerMinutes);
}
/**
* @param array<int, string> $seenExternalIds
*/
private function cancelMissingEvents(array $seenExternalIds): int
{
$events = CalendarEvent::query()
->where('source', 'caldav')
->whereNull('deleted_at')
->whereNotIn('external_id', $seenExternalIds ?: [''])
->get();
foreach ($events as $event) {
$event->update(['deleted_at' => Carbon::now()]);
$event->reminders()
->whereIn('status', ['pending', 'failed'])
->update([
'status' => 'cancelled',
'last_error' => 'Evenement absent de la derniere synchronisation.',
]);
}
return $events->count();
}
}