store/app/Jobs/SendPushNotificationJob.php

144 lines
3.9 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\Notification;
use App\Models\PushSubscription;
use App\Models\SystemConfiguration;
use App\Models\User;
use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Queue\Queueable;
use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;
class SendPushNotificationJob implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $title,
private readonly string $body,
private readonly string $url = '/admin/dashboard',
private readonly array $roles = [],
private readonly ?int $userId = null,
) {}
public function handle(): void
{
$this->persistNotifications();
$this->sendWebPush();
}
private function persistNotifications(): void
{
$userIds = $this->resolveUserIds();
foreach ($userIds as $uid) {
Notification::create([
'user_id' => $uid,
'title' => $this->title,
'body' => $this->body,
'url' => $this->url,
]);
}
}
private function sendWebPush(): void
{
$subscriptions = $this->resolveSubscriptions();
if ($subscriptions->isEmpty()) {
return;
}
$icon = $this->resolveIcon();
$webPush = new WebPush([
'VAPID' => [
'subject' => config('app.url'),
'publicKey' => config('webpush.vapid.public_key'),
'privateKey' => config('webpush.vapid.private_key'),
],
]);
$payload = json_encode([
'title' => $this->title,
'body' => $this->body,
'icon' => $icon,
'url' => $this->url,
]);
foreach ($subscriptions as $subscription) {
$sub = Subscription::create([
'endpoint' => $subscription->endpoint,
'publicKey' => $subscription->public_key,
'authToken' => $subscription->auth_token,
'contentEncoding' => 'aes128gcm',
]);
$webPush->queueNotification($sub, $payload);
}
$expiredEndpoints = [];
foreach ($webPush->flush() as $report) {
if ($report->isSubscriptionExpired()) {
$expiredEndpoints[] = $report->getEndpoint();
}
}
if (! empty($expiredEndpoints)) {
PushSubscription::whereIn('endpoint', $expiredEndpoints)->delete();
}
}
private function resolveIcon(): string
{
try {
$configuration = SystemConfiguration::instance();
$configuration->load('media');
$logo = MediaPresenter::first($configuration, 'logo');
return $logo['url'] ?? config('app.url').'/assets/logo.png';
} catch (\Throwable) {
return config('app.url').'/assets/logo.png';
}
}
private function resolveSubscriptions()
{
$query = PushSubscription::query();
if ($this->userId !== null) {
$query->where('user_id', $this->userId);
} elseif (! empty($this->roles)) {
$query->whereHas('user', function (Builder $userQuery): void {
$userQuery->whereHas('roles', fn (Builder $roleQuery) => $roleQuery->whereIn('name', $this->roles));
});
}
return $query->get();
}
/**
* @return list<int>
*/
private function resolveUserIds(): array
{
if ($this->userId !== null) {
return [$this->userId];
}
if (! empty($this->roles)) {
return User::query()
->whereHas('roles', fn (Builder $q) => $q->whereIn('name', $this->roles))
->pluck('id')
->all();
}
return User::query()->pluck('id')->all();
}
}