kithkin/app/Http/Controllers/AttendeeController.php

164 lines
5.0 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Card;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Sabre\VObject\Reader;
class AttendeeController extends Controller
{
public function suggest(Request $request)
{
$q = trim((string) $request->input('q', $request->input('attendee', '')));
if ($q === '' || mb_strlen($q) < 2) {
return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]);
}
$principal = $request->user()?->principal_uri;
if (!$principal) {
return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]);
}
$query = Str::lower($q);
$contacts = [];
$seen = [];
$cards = Card::query()
->select('cards.carddata')
->join('addressbooks', 'addressbooks.id', '=', 'cards.addressbookid')
->where('addressbooks.principaluri', $principal)
->whereRaw('LOWER(cards.carddata) LIKE ?', ['%' . $query . '%'])
->orderByDesc('cards.lastmodified')
->limit(200)
->get();
foreach ($cards as $card) {
$parsed = $this->parseCardEmails((string) $card->carddata);
$name = $parsed['name'];
foreach ($parsed['emails'] as $email) {
$emailLc = Str::lower($email);
$matches = str_contains($emailLc, $query)
|| ($name !== '' && str_contains(Str::lower($name), $query));
if (!$matches || isset($seen[$emailLc])) {
continue;
}
$seen[$emailLc] = true;
$contacts[] = [
'email' => $emailLc,
'name' => $name !== '' ? $name : null,
'attendee_uri' => 'mailto:' . $emailLc,
'source' => 'contact',
];
}
}
if (filter_var($q, FILTER_VALIDATE_EMAIL)) {
$emailLc = Str::lower($q);
if (!isset($seen[$emailLc])) {
$contacts[] = [
'email' => $emailLc,
'name' => null,
'attendee_uri' => 'mailto:' . $emailLc,
'source' => 'email',
];
}
}
if (empty($contacts)) {
return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]);
}
usort($contacts, function (array $a, array $b) use ($query) {
$scoreA = $this->score($a, $query);
$scoreB = $this->score($b, $query);
if ($scoreA !== $scoreB) {
return $scoreA <=> $scoreB;
}
return strcmp($a['email'], $b['email']);
});
$contacts = array_slice($contacts, 0, 8);
$emails = array_values(array_unique(array_map(fn ($item) => $item['email'], $contacts)));
$knownUsers = User::query()
->whereIn('email', $emails)
->pluck('email')
->map(fn ($email) => Str::lower((string) $email))
->flip()
->all();
foreach ($contacts as &$item) {
$item['verified'] = isset($knownUsers[$item['email']]);
}
unset($item);
return response()->view('event.partials.attendee-suggestions', [
'suggestions' => $contacts,
]);
}
private function parseCardEmails(string $carddata): array
{
$name = '';
$emails = [];
try {
$vcard = Reader::read($carddata);
$name = trim((string) ($vcard->FN ?? ''));
foreach ($vcard->select('EMAIL') as $emailProp) {
$email = Str::lower(trim((string) $emailProp->getValue()));
if ($email !== '') {
$emails[] = $email;
}
}
} catch (\Throwable $e) {
if (preg_match('/^FN:(.+)$/mi', $carddata, $fn)) {
$name = trim($fn[1]);
}
if (preg_match_all('/^EMAIL[^:]*:(.+)$/mi', $carddata, $matches)) {
foreach (($matches[1] ?? []) as $emailRaw) {
$email = Str::lower(trim((string) $emailRaw));
if ($email !== '') {
$emails[] = $email;
}
}
}
}
return [
'name' => $name,
'emails' => array_values(array_unique($emails)),
];
}
private function score(array $item, string $query): int
{
$email = Str::lower((string) ($item['email'] ?? ''));
$name = Str::lower((string) ($item['name'] ?? ''));
$source = (string) ($item['source'] ?? '');
if ($email === $query) {
return 0;
}
if ($email !== '' && str_starts_with($email, $query)) {
return 1;
}
if ($name !== '' && str_starts_with($name, $query)) {
return 2;
}
if ($source === 'contact') {
return 3;
}
return 4;
}
}