Files
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

234 lines
9.5 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// send_fcm.php - FCM HTTP v1 Sender (Internal use only)
header('Content-Type: application/json; charset=utf-8');
// 🔐 Require internal API key for authentication if set
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
$expectedKey = getenv('FCM_INTERNAL_API_KEY');
if (!empty($expectedKey) && !hash_equals($expectedKey, $apiKey)) {
// ‏فشل صامت خطير: التطبيقان لا يرسلان x-api-key إطلاقاً، فلحظة ضبط
// ‏FCM_INTERNAL_API_KEY في البيئة تموت **كل** الرسائل والمكالمات بينهما.
error_log('[SEND_FCM] REJECTED 403 — FCM_INTERNAL_API_KEY set but request sent no x-api-key');
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
function resolveServiceAccountPath(): string {
$envPath = getenv('FIREBASE_SERVICE_ACCOUNT_PATH') ?: '';
if (!empty($envPath) && file_exists($envPath)) {
return $envPath;
}
// if (file_exists('/keys/firebase_service_account.json')) {
// return '/keys/firebase_service_account.json';
// }
if (file_exists('/keys/service-account.json')) {
return '/keys/service-account.json';
}
// if (file_exists(__DIR__ . '/../../keys/firebase_service_account.json')) {
// return __DIR__ . '/../../keys/firebase_service_account.json';
// }
return __DIR__ . '/service-account.json';
}
$serviceAccountFile = resolveServiceAccountPath();
// السماح فقط بـ POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Only POST allowed.']);
exit;
}
// استقبال البيانات
$json_input = file_get_contents('php://input');
$requestData = json_decode($json_input, true);
$target = $requestData['target'] ?? null;
$title = $requestData['title'] ?? null;
$body = $requestData['body'] ?? null;
$isTopic = $requestData['isTopic'] ?? false;
$tone = $requestData['tone'] ?? 'default';
$customData = $requestData['data'] ?? [];
if (!$target) {
// ‏يعني أن المُرسِل لا يملك توكن الطرف الآخر — عادةً tokenPassenger أو
// ‏driverToken فارغ في التطبيق لأنه لم يصله في حمولة القبول.
error_log('[SEND_FCM] REJECTED 400 — empty target. category='
. (string)($requestData['data']['category'] ?? '?'));
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing: target, title, or body.']);
exit;
}
// ============================================================================
// دالة Base64 URL-Safe Encoding (ضرورية للـ JWT)
// ============================================================================
function base64UrlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
// ============================================================================
// دالة المصادقة (Google OAuth2)
// ============================================================================
function getAccessToken($credentialsPath) {
if (!file_exists($credentialsPath)) return null;
$credentials = json_decode(file_get_contents($credentialsPath), true);
$clientEmail = $credentials['client_email'];
$privateKey = $credentials['private_key'];
$now = time();
$header = base64UrlEncode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$claim = base64UrlEncode(json_encode([
'iss' => $clientEmail,
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now
]));
$signature = '';
openssl_sign("$header.$claim", $signature, $privateKey, 'SHA256');
$jwt = "$header.$claim." . base64UrlEncode($signature);
$ch = curl_init("https://oauth2.googleapis.com/token");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// بلا مهلة كان الطلب يقدر يعلّق حتى مهلة PHP نفسها، فتُسقط الرسالة بصمت
// وهذا أحد أسباب "الإشعار مرات يوصل ومرات لا".
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
$res = curl_exec($ch);
curl_close($ch);
return json_decode($res, true)['access_token'] ?? null;
}
// الحصول على Access Token
$accessToken = getAccessToken($serviceAccountFile);
if (!$accessToken) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Failed to get Access Token.']);
exit;
}
// جلب Project ID
$creds = json_decode(file_get_contents($serviceAccountFile), true);
$projectId = $creds['project_id'];
$fcmUrl = "https://fcm.googleapis.com/v1/projects/$projectId/messages:send";
// ============================================================================
// بناء هيكل الرسالة
// ============================================================================
// 🔧 توحيد نغمة أندرويد مع FcmService::send — هناك 'ding' تُترجم إلى 'default'،
// وهنا كانت تُمرَّر كما هي فيبحث أندرويد عن ملف صوت اسمه "ding" وقد لا يوجد
// فيصل الإشعار بلا صوت (أو لا يُلفت النظر إطلاقاً).
$androidSound = ($tone === 'ding' || $tone === 'default') ? 'default' : $tone;
$messagePayload = [
'message' => [
'notification' => [
'title' => $title,
'body' => $body
],
'android' => [
'priority' => 'HIGH',
'notification' => [
'sound' => $androidSound,
'channel_id' => 'high_importance_channel' // تأكد من تطابقه مع Android
]
],
'apns' => [
'headers' => ['apns-priority' => '10'],
'payload' => [
'aps' => [
'sound' => ($tone === 'ding' || $tone === 'default') ? 'default' : (str_ends_with($tone, '.caf') ? $tone : $tone . '.caf'),
'content-available' => 1
]
]
]
]
];
// تحديد الهدف (Topic أو Token)
if ($isTopic) {
$messagePayload['message']['topic'] = $target;
} else {
$messagePayload['message']['token'] = $target;
}
// ============================================================================
// 🔥 معالجة Data Payload (يجب أن تكون String: String فقط)
// ============================================================================
// FcmService::send يحقن title/body/tone/category/type داخل data، وتطبيق الراكب
// يقرأ message.data['title'] أولاً. هذا المسار كان لا يحقنها، فأي رسالة تمرّ من
// هنا تظهر بعنوان فارغ عند العميل الذي يعتمد على data. نوحّد السلوك.
$customData = array_merge(is_array($customData) ? $customData : [], [
'title' => (string)$title,
'body' => (string)$body,
'tone' => (string)$tone,
]);
if (!empty($customData)) {
$processedData = [];
foreach ($customData as $key => $val) {
if (is_array($val) || is_object($val)) {
// تحويل المصفوفات/الكائنات إلى JSON String
$processedData[$key] = json_encode($val, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
// تحويل أي قيمة أخرى إلى String
$processedData[$key] = (string)$val;
}
}
$messagePayload['message']['data'] = $processedData;
}
// ============================================================================
// الإرسال الفعلي إلى FCM
// ============================================================================
$ch = curl_init($fcmUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json; charset=UTF-8'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($messagePayload, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// ‏هذه النقطة هي مسار كل الرسائل والمكالمات بين الراكب والسائق (التطبيقان
// ‏يستدعيانها مباشرة)، ولم تكن تسجّل شيئاً إطلاقاً — بخلاف FcmService التي
// ‏تطبع [FCM_DEBUG]. فكان فشل الرسائل غير قابل للتشخيص: لا سبب ولا رد Google.
$logCategory = (string)($customData['category'] ?? '');
$logTarget = $isTopic ? "topic:$target" : substr((string)$target, 0, 16) . '…';
error_log(
"[SEND_FCM] category=$logCategory target=$logTarget http=$httpCode"
. " len=" . strlen((string)$target)
. ($httpCode == 200 ? '' : " response=$result")
);
// الرد
if ($httpCode == 200) {
echo json_encode([
'status' => 'success',
'message' => 'Notification sent successfully',
'fcm_response' => json_decode($result)
], JSON_UNESCAPED_UNICODE);
} else {
http_response_code($httpCode);
echo json_encode([
'status' => 'error',
'message' => 'FCM request failed',
'fcm_response' => json_decode($result)
], JSON_UNESCAPED_UNICODE);
}
?>