|
@@ -0,0 +1,132 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Services\Fcm;
|
|
|
|
|
+
|
|
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
|
|
+use Throwable;
|
|
|
|
|
+
|
|
|
|
|
+class FcmService
|
|
|
|
|
+{
|
|
|
|
|
+ protected string $projectId;
|
|
|
|
|
+ protected string $accessToken;
|
|
|
|
|
+
|
|
|
|
|
+ public function __construct()
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->projectId = config('fcm.project_id');
|
|
|
|
|
+ $this->accessToken = $this->getAccessToken();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function send(string $token, string $title, string $body, ?string $image = null): ?array
|
|
|
|
|
+ {
|
|
|
|
|
+ $url = "https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send";
|
|
|
|
|
+
|
|
|
|
|
+ $message = [
|
|
|
|
|
+ 'message' => [
|
|
|
|
|
+ 'token' => $token,
|
|
|
|
|
+ 'notification' => [
|
|
|
|
|
+ 'title' => $title,
|
|
|
|
|
+ 'body' => $body,
|
|
|
|
|
+ 'image' => $image,
|
|
|
|
|
+ ],
|
|
|
|
|
+ 'android' => [
|
|
|
|
|
+ 'priority' => 'high',
|
|
|
|
|
+ ],
|
|
|
|
|
+ 'apns' => [
|
|
|
|
|
+ 'headers' => [
|
|
|
|
|
+ 'apns-priority' => '10',
|
|
|
|
|
+ ],
|
|
|
|
|
+ 'payload' => [
|
|
|
|
|
+ 'aps' => [
|
|
|
|
|
+ 'sound' => 'default',
|
|
|
|
|
+ 'badge' => 1,
|
|
|
|
|
+ ],
|
|
|
|
|
+ ],
|
|
|
|
|
+ ],
|
|
|
|
|
+ ],
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ $response = Http::withToken($this->accessToken)->post($url, $message);
|
|
|
|
|
+ $result = $response->json();
|
|
|
|
|
+
|
|
|
|
|
+ Log::channel('fcm')->info('FCM send', [
|
|
|
|
|
+ 'token' => substr($token, 0, 20) . '...',
|
|
|
|
|
+ 'title' => $title,
|
|
|
|
|
+ 'status' => $response->status(),
|
|
|
|
|
+ 'response' => $result,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if ($response->failed()) {
|
|
|
|
|
+ Log::channel('fcm')->error('FCM send failed', [
|
|
|
|
|
+ 'token' => substr($token, 0, 20) . '...',
|
|
|
|
|
+ 'status' => $response->status(),
|
|
|
|
|
+ 'error' => $result,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $result;
|
|
|
|
|
+ } catch (Throwable $e) {
|
|
|
|
|
+ Log::channel('fcm')->error('FCM exception', [
|
|
|
|
|
+ 'token' => substr($token, 0, 20) . '...',
|
|
|
|
|
+ 'message' => $e->getMessage(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function getAccessToken(): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $clientEmail = config('fcm.client_email');
|
|
|
|
|
+ $privateKey = config('fcm.private_key');
|
|
|
|
|
+
|
|
|
|
|
+ $jwt = $this->createJwt($clientEmail, $privateKey);
|
|
|
|
|
+
|
|
|
|
|
+ return $this->exchangeJwtForAccessToken($jwt);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function createJwt(string $clientEmail, string $privateKey): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
|
|
|
|
|
+ $now = time();
|
|
|
|
|
+
|
|
|
|
|
+ $payload = json_encode([
|
|
|
|
|
+ 'iss' => $clientEmail,
|
|
|
|
|
+ 'sub' => $clientEmail,
|
|
|
|
|
+ 'aud' => 'https://oauth2.googleapis.com/token',
|
|
|
|
|
+ 'iat' => $now,
|
|
|
|
|
+ 'exp' => $now + 3600,
|
|
|
|
|
+ 'scope' => 'https://www.googleapis.com/auth/cloud-platform',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $base64Header = $this->base64UrlEncode($header);
|
|
|
|
|
+ $base64Payload = $this->base64UrlEncode($payload);
|
|
|
|
|
+
|
|
|
|
|
+ $signature = '';
|
|
|
|
|
+ openssl_sign("{$base64Header}.{$base64Payload}", $signature, $privateKey, 'sha256');
|
|
|
|
|
+
|
|
|
|
|
+ return "{$base64Header}.{$base64Payload}." . $this->base64UrlEncode($signature);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function exchangeJwtForAccessToken(string $jwt): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [
|
|
|
|
|
+ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
|
|
|
+ 'assertion' => $jwt,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $data = $response->json();
|
|
|
|
|
+
|
|
|
|
|
+ if (isset($data['access_token'])) {
|
|
|
|
|
+ return $data['access_token'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ throw new \RuntimeException('FCM: Failed to obtain access token: ' . $response->body());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function base64UrlEncode(string $data): string
|
|
|
|
|
+ {
|
|
|
|
|
+ return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|