104 lines
4.0 KiB
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
|
|
}
|
|
}
|
|
}
|
|
}
|