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 $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(); } }