Add f7push v0.1: FCM device registry and push API for F7cloud.
Portable occ-based config, OCS endpoints for APK registration, notification relay, and server-side push dispatch.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\F7Push\Service;
|
||||
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FirebaseClient {
|
||||
private const TOKEN_CACHE_KEY = 'f7push_fcm_access_token';
|
||||
private const TOKEN_TTL = 3300;
|
||||
|
||||
public function __construct(
|
||||
private ConfigService $config,
|
||||
private IClientService $clientService,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function sendToToken(string $fcmToken, string $title, string $body, ?string $url, string $priority): bool {
|
||||
if (!$this->config->isFirebaseConfigured()) {
|
||||
$this->logger->info('f7push: Firebase not configured, skip FCM send', ['app' => 'f7push']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$accessToken = $this->getAccessToken();
|
||||
if ($accessToken === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$projectId = $this->config->getFirebaseProjectId();
|
||||
$endpoint = 'https://fcm.googleapis.com/v1/projects/' . rawurlencode($projectId) . '/messages:send';
|
||||
|
||||
$notification = [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
];
|
||||
$data = [];
|
||||
if ($url !== null && $url !== '') {
|
||||
$data['url'] = $url;
|
||||
}
|
||||
$data['click_action'] = 'FLUTTER_NOTIFICATION_CLICK';
|
||||
|
||||
$message = [
|
||||
'message' => [
|
||||
'token' => $fcmToken,
|
||||
'notification' => $notification,
|
||||
'data' => $data,
|
||||
'android' => [
|
||||
'priority' => $priority === 'high' ? 'HIGH' : 'NORMAL',
|
||||
'notification' => [
|
||||
'channel_id' => $priority === 'high' ? 'f7cloud_calls' : 'f7cloud_default',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
try {
|
||||
$response = $client->post($endpoint, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $accessToken,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => json_encode($message, JSON_THROW_ON_ERROR),
|
||||
'timeout' => 15,
|
||||
]);
|
||||
return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('f7push: FCM send failed', [
|
||||
'app' => 'f7push',
|
||||
'exception' => $e,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getAccessToken(): ?string {
|
||||
$cache = $this->getCache();
|
||||
$cached = $cache->get(self::TOKEN_CACHE_KEY);
|
||||
if (is_string($cached) && $cached !== '') {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$credentialsJson = $this->config->getFirebaseCredentials();
|
||||
try {
|
||||
/** @var array<string, mixed> $credentials */
|
||||
$credentials = json_decode($credentialsJson, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('f7push: invalid firebase_credentials JSON', ['app' => 'f7push', 'exception' => $e]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$clientEmail = (string)($credentials['client_email'] ?? '');
|
||||
$privateKey = (string)($credentials['private_key'] ?? '');
|
||||
if ($clientEmail === '' || $privateKey === '') {
|
||||
$this->logger->error('f7push: firebase_credentials missing client_email or private_key', ['app' => 'f7push']);
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$header = $this->base64UrlEncode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'], JSON_THROW_ON_ERROR));
|
||||
$claims = $this->base64UrlEncode(json_encode([
|
||||
'iss' => $clientEmail,
|
||||
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
|
||||
'aud' => 'https://oauth2.googleapis.com/token',
|
||||
'iat' => $now,
|
||||
'exp' => $now + 3600,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$input = $header . '.' . $claims;
|
||||
|
||||
$signature = '';
|
||||
$ok = openssl_sign($input, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
if (!$ok) {
|
||||
$this->logger->error('f7push: openssl_sign failed for Firebase JWT', ['app' => 'f7push']);
|
||||
return null;
|
||||
}
|
||||
|
||||
$jwt = $input . '.' . $this->base64UrlEncode($signature);
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
try {
|
||||
$response = $client->post('https://oauth2.googleapis.com/token', [
|
||||
'body' => http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt,
|
||||
]),
|
||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||||
'timeout' => 15,
|
||||
]);
|
||||
$data = json_decode($response->getBody(), true);
|
||||
$token = is_array($data) ? (string)($data['access_token'] ?? '') : '';
|
||||
if ($token === '') {
|
||||
return null;
|
||||
}
|
||||
$cache->set(self::TOKEN_CACHE_KEY, $token, self::TOKEN_TTL);
|
||||
return $token;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('f7push: Firebase OAuth token exchange failed', ['app' => 'f7push', 'exception' => $e]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCache(): ICache {
|
||||
return $this->cacheFactory->createDistributed('f7push');
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $data): string {
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user