configured()) { Log::debug('webpush: VAPID not configured, skip'); return false; } $results = $this->dispatch([$sub], $title, $body, $url, $tag); return $results['sent'] > 0; } /** * Send to every subscription of a user (a person may have several devices). * * @return array{sent:int, pruned:int} */ public function sendToUser(int $userId, string $title, string $body, ?string $url = null, ?string $tag = null): array { $subs = PushSubscription::where('user_id', $userId)->get(); return $this->dispatch($subs, $title, $body, $url, $tag); } /** * @param iterable $subs * @return array{sent:int, pruned:int} */ public function dispatch(iterable $subs, string $title, string $body, ?string $url = null, ?string $tag = null): array { if (! $this->configured()) return ['sent' => 0, 'pruned' => 0]; $webPush = new WebPush([ 'VAPID' => [ 'subject' => config('webpush.vapid.subject'), 'publicKey' => config('webpush.vapid.public_key'), 'privateKey' => config('webpush.vapid.private_key'), ], ]); $webPush->setDefaultOptions(['TTL' => (int) config('webpush.ttl', 2419200)]); $payload = json_encode([ 'title' => $title, 'body' => $body, 'url' => $url ?? '/app', 'tag' => $tag ?? 'autocrm', ]); // Index by endpoint so we can prune by the report's endpoint. $byEndpoint = []; foreach ($subs as $s) { $byEndpoint[$s->endpoint] = $s; $webPush->queueNotification( PushSub::create([ 'endpoint' => $s->endpoint, 'publicKey' => $s->public_key, 'authToken' => $s->auth_token, 'contentEncoding' => $s->content_encoding ?: 'aesgcm', ]), $payload ); } $sent = 0; $pruned = 0; foreach ($webPush->flush() as $report) { $endpoint = $report->getRequest()->getUri()->__toString(); if ($report->isSuccess()) { $sent++; } elseif ($report->isSubscriptionExpired()) { // 404/410 — device unsubscribed; remove stored row. if (isset($byEndpoint[$endpoint])) { $byEndpoint[$endpoint]->delete(); $pruned++; } else { PushSubscription::where('endpoint', $endpoint)->delete(); $pruned++; } } else { Log::warning('webpush: delivery failed', [ 'endpoint' => $endpoint, 'reason' => $report->getReason(), ]); } } return ['sent' => $sent, 'pruned' => $pruned]; } }