tutoring/app/Jobs/SyncNextcloudBusyJob.php

104 lines
4.0 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\ExternalBusyWindow;
use App\Models\ExternalCalendar;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Sabre\VObject\Reader;
class SyncNextcloudBusyJob implements ShouldQueue
{
use Dispatchable, Queueable, SerializesModels;
public function handle(): void
{
\Log::info('SyncNextcloudBusyJob running at '.now());
// Choose which calendars to sync (you can expand later to many).
$calendars = ExternalCalendar::where('is_enabled', true)->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
}
}
}
}