get(); if ($calendars->isEmpty()) return; $windowStart = now()->utc(); $windowEnd = now()->addWeeks(6)->utc(); // adjust horizon if needed foreach ($calendars as $cal) { try { $resp = Http::timeout(20)->get($cal->url); if (!$resp->ok()) { // log and continue \Log::warning("ICS fetch failed", ['cal' => $cal->id, 'status' => $resp->status()]); continue; } $ics = $resp->body(); if (!trim($ics)) continue; $vcal = Reader::read($ics); // VCalendar // Expand recurring events into single instances within window // Mutates the calendar to contain expanded VEVENTs $vcal = $vcal->expand( new \DateTime($windowStart->format('Y-m-d\TH:i:s\Z')), new \DateTime($windowEnd->format('Y-m-d\TH:i:s\Z')) ); // Remove existing windows in this horizon (simple strategy) ExternalBusyWindow::where('external_calendar_id', $cal->id) ->whereBetween('starts_at', [$windowStart, $windowEnd]) ->delete(); $rows = []; $nowTs = now(); // Iterate expanded events foreach ($vcal->select('VEVENT') as $ve) { // Treat TRANSPARENT as free; OPAQUE as busy (default busy) $transp = strtoupper((string)($ve->TRANSP ?? 'OPAQUE')); if ($transp === 'TRANSPARENT') continue; // Safely extract start/end in UTC $startDt = $ve->DTSTART->getDateTime(new \DateTimeZone('UTC')); // DTEND may be absent; if so, use DTSTART as a zero-length block (or add a default length) $endDt = isset($ve->DTEND) ? $ve->DTEND->getDateTime(new \DateTimeZone('UTC')) : (clone $startDt); // Skip malformed ranges if ($endDt <= $startDt) continue; // Only keep those overlapping our window $startsAt = CarbonImmutable::instance($startDt); $endsAt = CarbonImmutable::instance($endDt); if ($endsAt <= $windowStart || $startsAt >= $windowEnd) { continue; } $rows[] = [ 'external_calendar_id' => $cal->id, 'starts_at' => $startsAt->toDateTimeString(), 'ends_at' => $endsAt->toDateTimeString(), 'transparency'=> 'OPAQUE', 'last_seen_at'=> $nowTs->toDateTimeString(), 'created_at' => $nowTs->toDateTimeString(), 'updated_at' => $nowTs->toDateTimeString(), ]; } if (!empty($rows)) { // Bulk insert ExternalBusyWindow::insert($rows); } } catch (\Throwable $e) { \Log::error("ICS sync error", ['cal' => $cal->id, 'e' => $e->getMessage()]); // Continue with other calendars } } } }