Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user