kithkin/app/Services/Event/EventAttendeeSynchronizer.php

237 lines
7.0 KiB
PHP

<?php
namespace App\Services\Event;
use App\Models\Event;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader;
class EventAttendeeSynchronizer
{
public function syncFromCalendarData(Event $event, ?string $calendarData = null): void
{
$attendees = $this->extractFromCalendarData($calendarData ?? $event->calendardata);
$this->syncRows($event, $attendees);
}
public function syncRows(Event $event, array $attendees): void
{
$rows = $this->normalizeRows($attendees);
$event->attendees()->delete();
if (empty($rows)) {
return;
}
foreach ($rows as &$row) {
$email = $row['email'] ?? null;
if (!$email) {
$row['attendee_user_id'] = null;
continue;
}
$row['attendee_user_id'] = User::query()
->whereRaw('lower(email) = ?', [Str::lower($email)])
->value('id');
}
unset($row);
$event->attendees()->createMany($rows);
}
public function extractFromCalendarData(?string $calendarData): array
{
if (!$calendarData) {
return [];
}
try {
$vcalendar = Reader::read($calendarData);
} catch (\Throwable $e) {
return [];
}
$vevent = $vcalendar->select('VEVENT')[0] ?? null;
if (!$vevent instanceof VEvent) {
return [];
}
return $this->extractFromVevent($vevent);
}
public function extractFromVevent(VEvent $vevent): array
{
$items = [];
foreach ($vevent->select('ATTENDEE') as $attendee) {
$params = $this->extractParams($attendee);
$uri = $this->normalizeAttendeeUri((string) $attendee->getValue());
$email = $this->extractEmail($uri, $params);
$items[] = [
'attendee_uri' => $uri,
'email' => $email,
'name' => $params['CN'] ?? null,
'role' => isset($params['ROLE']) ? strtoupper($params['ROLE']) : null,
'partstat' => isset($params['PARTSTAT']) ? strtoupper($params['PARTSTAT']) : null,
'cutype' => isset($params['CUTYPE']) ? strtoupper($params['CUTYPE']) : null,
'rsvp' => $this->normalizeBooleanParam($params['RSVP'] ?? null),
'is_organizer' => false,
'extra' => $this->extraParams($params, [
'CN',
'ROLE',
'PARTSTAT',
'CUTYPE',
'RSVP',
'EMAIL',
]),
];
}
if (isset($vevent->ORGANIZER)) {
$organizer = $vevent->ORGANIZER;
$params = $this->extractParams($organizer);
$uri = $this->normalizeAttendeeUri((string) $organizer->getValue());
$email = $this->extractEmail($uri, $params);
$items[] = [
'attendee_uri' => $uri,
'email' => $email,
'name' => $params['CN'] ?? null,
'role' => 'CHAIR',
'partstat' => null,
'cutype' => isset($params['CUTYPE']) ? strtoupper($params['CUTYPE']) : null,
'rsvp' => null,
'is_organizer' => true,
'extra' => $this->extraParams($params, ['CN', 'CUTYPE', 'EMAIL']),
];
}
return $items;
}
private function normalizeRows(array $attendees): array
{
$rows = [];
$seen = [];
foreach ($attendees as $item) {
$email = $this->normalizeEmail(Arr::get($item, 'email'));
$uri = $this->normalizeAttendeeUri(Arr::get($item, 'attendee_uri'));
if (!$uri && $email) {
$uri = 'mailto:' . $email;
}
if (!$uri && !$email) {
continue;
}
$key = Str::lower($uri ?: $email);
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$role = Arr::get($item, 'role');
$partstat = Arr::get($item, 'partstat');
$cutype = Arr::get($item, 'cutype');
$rows[] = [
'attendee_uri' => $uri,
'email' => $email,
'name' => $this->normalizeString(Arr::get($item, 'name')),
'role' => $role ? strtoupper((string) $role) : null,
'partstat' => $partstat ? strtoupper((string) $partstat) : null,
'cutype' => $cutype ? strtoupper((string) $cutype) : null,
'rsvp' => Arr::exists($item, 'rsvp') ? (bool) Arr::get($item, 'rsvp') : null,
'is_organizer' => (bool) Arr::get($item, 'is_organizer', false),
'extra' => is_array(Arr::get($item, 'extra')) ? Arr::get($item, 'extra') : null,
];
}
return $rows;
}
private function extractParams($property): array
{
$params = [];
foreach ($property->parameters() as $parameter) {
$params[strtoupper((string) $parameter->name)] = trim((string) $parameter->getValue());
}
return $params;
}
private function extractEmail(?string $uri, array $params): ?string
{
$fromParam = $this->normalizeEmail($params['EMAIL'] ?? null);
if ($fromParam) {
return $fromParam;
}
if (!$uri) {
return null;
}
if (str_starts_with(Str::lower($uri), 'mailto:')) {
return $this->normalizeEmail(substr($uri, 7));
}
return str_contains($uri, '@') ? $this->normalizeEmail($uri) : null;
}
private function normalizeAttendeeUri(mixed $uri): ?string
{
$value = trim((string) $uri);
if ($value === '') {
return null;
}
if (str_contains($value, '@') && !str_contains($value, ':')) {
return 'mailto:' . Str::lower($value);
}
if (str_starts_with(Str::lower($value), 'mailto:')) {
return 'mailto:' . Str::lower(substr($value, 7));
}
return $value;
}
private function normalizeEmail(mixed $value): ?string
{
$email = trim((string) $value);
if ($email === '') {
return null;
}
return Str::lower($email);
}
private function normalizeString(mixed $value): ?string
{
$string = trim((string) $value);
return $string === '' ? null : $string;
}
private function normalizeBooleanParam(?string $value): ?bool
{
if ($value === null || $value === '') {
return null;
}
return in_array(strtoupper($value), ['1', 'TRUE', 'YES'], true);
}
private function extraParams(array $params, array $known): ?array
{
$knownMap = array_fill_keys($known, true);
$extra = array_diff_key($params, $knownMap);
return empty($extra) ? null : $extra;
}
}