From 143146c1b49d621d0e83acc8f36991c3c0ac0e7f Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 26 Jul 2026 15:47:23 +0300 Subject: [PATCH] =?UTF-8?q?=D8=A5=D8=B5=D9=84=D8=A7=D8=AD=20=D8=B3=D9=88?= =?UTF-8?q?=D9=83=D9=8A=D8=AA=20=D8=A7=D9=84=D8=B1=D8=A7=D9=83=D8=A8=20?= =?UTF-8?q?=D9=88=D8=AA=D9=88=D8=AD=D9=8A=D8=AF=20=D9=85=D8=B3=D8=A7=D8=B1?= =?UTF-8?q?=D8=A7=D8=AA=20=D8=A7=D9=84=D8=A5=D8=B4=D8=B9=D8=A7=D8=B1=D8=A7?= =?UTF-8?q?=D8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit سوكيت الراكب (سبب عدم ظهور معلومات السائق عند القبول): - تطبيق الراكب كان يرسل id فقط بلا jwt، و passenger_socket.php يرفض أي اتصال بلا jwt ⇒ الراكب لا ينضم لغرفته أبداً ولا يستلم ride_status_change ولا driver_location_update. تظهر حالة القبول عبر الـ polling فقط بينما driver_info يصل بالسوكيت وحده. (سوكيت السائق يعتبر الـ jwt اختيارياً، ومن هنا جاء التباين بين التطبيقين.) - cancelled_by_driver كان يسقط من switch حالات الراكب فيبقى معلّقاً بعد إلغاء السائق. - حماية socket (late) من القراءة قبل التهيئة عند الانسحاب بلا jwt. الإشعارات والرسائل (سبب "مرات توصل ومرات لا"): - جدول tokens يخزّن توكن الراكب مشفّراً، و getRideWaiting.php كان يرجعه بلا فك تشفير ⇒ من يقبل من قائمة السوق يحمل blob مشفّراً يستخدمه كـ FCM target فيرفضه FCM بـ 400: لا إشعار قبول ولا رسائل. ومن يقبل من الـ dispatch/FCM يحمل نصاً صريحاً فتعمل. الفرق كان في طريقة القبول. - acceptRide.php يحلّ التوكن من القاعدة دائماً ولا يثق بالعميل (أصحّ أمنياً). - market_new_ride كان لا يحمل passengerId ولا الإحداثيات فتصل "null"؛ أُضيفت بلا أي PII لأن الحمولة تُبَثّ لكل سائق قريب لا للفائز فقط. - send_fcm.php: مهلة على OAuth (كان يعلّق حتى مهلة PHP فتُسقط الرسالة بصمت)، توحيد ding→default لأندرويد، وحقن title/body/tone في data مطابقةً لـ FcmService. - تطبيق السائق يقرأ title/body من data أولاً مثل الراكب، ولا يعرض فقاعة فارغة للرسائل الصامتة. السوكيت والإعدادات: - forwardLocationToPassengerSocket كان يقرأ lat/lng والحمولة فيها latitude/longitude ⇒ المسافة تخرج ضخمة والـ throttle معطّل تماماً فيُعاد التوجيه مع كل نبضة GPS. - notifyPassengerOnRideServer كان يرجع null بصمت مطلق عند حجب العنوان. - العنوان الافتراضي لسيرفر الموقع كان nginx/loction_server/driver_socket.php وهو ديمون Workerman لا يُخدَم عبر nginx ⇒ صار socket_driver:2021. (LOCATION_API_URL بقي على nginx لأن api_get_nearby.php سكربت عادي.) - ride_server/passenger_socket.php (النسخة التي يشغّلها Docker) كانت ناقصة كل كود مواصلاتي الموجود في passenger_server/ ⇒ نُقل مع REDIS_HOST. - .env.example: ALLOWED_SOCKET_URLS يغطّي أسماء حاويات Docker، وإضافة PASSENGER_SOCKET_INTERNAL_URL. Co-Authored-By: Claude Opus 5 --- backend/functions.php | 16 ++-- backend/ride/firebase/send_fcm.php | 22 ++++- .../notificationCaptain/getRideWaiting.php | 15 ++++ backend/ride/rides/acceptRide.php | 32 +++++++ backend/ride/rides/add_ride.php | 34 ++++--- .../ride/rides/cancel_ride_by_passenger.php | 2 +- docker/.env.example | 8 +- loction_server/driver_socket.php | 15 ++-- ride_server/passenger_socket.php | 90 +++++++++++++++++++ .../controller/firebase/firbase_messge.dart | 12 +-- .../home/map/map_socket_controller.dart | 51 +++++++++-- 11 files changed, 258 insertions(+), 39 deletions(-) diff --git a/backend/functions.php b/backend/functions.php index ea118df5..99e365f7 100644 --- a/backend/functions.php +++ b/backend/functions.php @@ -59,7 +59,7 @@ function isAllowedSocketUrl(string $url): bool { } function sendToLocationServer($action, $data) { - $url = getenv('LOCATION_SERVER_URL') ?: 'http://nginx/loction_server/driver_socket.php'; + $url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021'; if (!isAllowedSocketUrl($url)) { error_log("[SSRF_BLOCKED] Attempted connection to: $url"); return; @@ -258,7 +258,7 @@ function getDistanceBetweenPoints($lat1, $lon1, $lat2, $lon2) { } // --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) --- function notifyDriversRideTaken($rideId, $winnerDriverId) { - $url = getenv('LOCATION_SERVER_URL') ?: 'http://nginx/loction_server/driver_socket.php'; + $url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021'; if (!isAllowedSocketUrl($url)) return; $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; @@ -280,7 +280,7 @@ function notifyDriversRideTaken($rideId, $winnerDriverId) { curl_close($ch); } function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) { - $url = getenv('LOCATION_SERVER_URL') ?: 'http://nginx/loction_server/driver_socket.php'; + $url = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021'; if (!isAllowedSocketUrl($url)) return null; $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; @@ -317,7 +317,13 @@ function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = n */ function notifyPassengerOnRideServer($passenger_id, $payload) { $url = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: (getenv('RIDE_SOCKET_URL') ?: 'http://socket_passenger:3031'); - if (!isAllowedSocketUrl($url)) return null; + if (!isAllowedSocketUrl($url)) { + // كان يرجع null بصمت تام — عكس sendToLocationServer — فإذا كان + // ALLOWED_SOCKET_URLS لا يغطّي عنوان السوكيت تسقط كل أحداث الراكب + // بلا أي أثر في اللوج. + error_log("[SOCKET_BLOCKED] Passenger socket URL not in ALLOWED_SOCKET_URLS: $url"); + return null; + } $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; if (empty($INTERNAL_KEY)) { @@ -363,7 +369,7 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa $countDrivers = count($driversData); error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers"); - $socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://nginx/loction_server/driver_socket.php'; + $socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021'; if (!isAllowedSocketUrl($socketUrl)) return; $internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; diff --git a/backend/ride/firebase/send_fcm.php b/backend/ride/firebase/send_fcm.php index f2521aa1..98abe732 100644 --- a/backend/ride/firebase/send_fcm.php +++ b/backend/ride/firebase/send_fcm.php @@ -92,9 +92,13 @@ function getAccessToken($credentialsPath) { '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; } @@ -114,6 +118,11 @@ $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' => [ @@ -123,7 +132,7 @@ $messagePayload = [ 'android' => [ 'priority' => 'HIGH', 'notification' => [ - 'sound' => $tone, + 'sound' => $androidSound, 'channel_id' => 'high_importance_channel' // تأكد من تطابقه مع Android ] ], @@ -149,6 +158,15 @@ if ($isTopic) { // ============================================================================ // 🔥 معالجة 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) { diff --git a/backend/ride/notificationCaptain/getRideWaiting.php b/backend/ride/notificationCaptain/getRideWaiting.php index 0ff91638..1219d981 100644 --- a/backend/ride/notificationCaptain/getRideWaiting.php +++ b/backend/ride/notificationCaptain/getRideWaiting.php @@ -131,6 +131,21 @@ try { $ride['first_name'] = $encryptionHelper->decryptData($ride['first_name'] ?? ''); $ride['email'] = $encryptionHelper->decryptData($ride['email'] ?? ''); + // جدول tokens يخزّن التوكن مشفّراً (ride/firebase/addToken.php). كان + // يُرجَع هنا كما هو، فيصل تطبيق السائق blob مشفّر يستخدمه كـ FCM target + // في كل رسالة للراكب ⇒ رفض 400 من FCM ولا يصل الراكب شيء. + if (!empty($ride['passengerToken'])) { + $decodedPassengerToken = false; + try { + $decodedPassengerToken = $encryptionHelper->decryptData($ride['passengerToken']); + } catch (\Throwable $eTok) { + $decodedPassengerToken = false; + } + if ($decodedPassengerToken !== false && $decodedPassengerToken !== null && $decodedPassengerToken !== '') { + $ride['passengerToken'] = trim($decodedPassengerToken); + } + } + $ride['start_location'] = $ride['start_lat'] . ',' . $ride['start_lng']; $ride['end_location'] = (!empty($ride['end_lat'])) ? $ride['end_lat'] . ',' . $ride['end_lng'] diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php index 93d34ea0..c01e0a74 100644 --- a/backend/ride/rides/acceptRide.php +++ b/backend/ride/rides/acceptRide.php @@ -198,6 +198,38 @@ try { $passengerIdValue = $passengerId->fetchColumn(); } + // 🆕 نحلّ توكن الراكب من القاعدة دائماً ولا نعتمد على ما يرسله التطبيق: + // • مسار الـ dispatch/FCM يرسل التوكن سليماً (نص صريح من add_ride.php). + // • مسار "سوق الرحلات" يرسله كما خرج من getRideWaiting.php أي **مشفّراً** + // (جدول tokens يخزّنه مشفّراً — انظر ride/firebase/addToken.php)، فكان + // FCM يرفضه بـ 400 ولا يصل الراكب أي إشعار قبول. + // القيمة القادمة من العميل تبقى fallback أخيراً فقط. + $passengerTokenFromClient = $passengerToken; + $passengerToken = ''; + if ($passengerIdValue) { + try { + $stmtTok = $con->prepare("SELECT token FROM tokens WHERE passengerID = ? LIMIT 1"); + $stmtTok->execute([$passengerIdValue]); + $rawPassengerToken = $stmtTok->fetchColumn(); + if (!empty($rawPassengerToken)) { + $decryptedToken = false; + try { + $decryptedToken = $encryptionHelper->decryptData($rawPassengerToken); + } catch (\Throwable $eTok) { + $decryptedToken = false; + } + $passengerToken = ($decryptedToken !== false && $decryptedToken !== null && $decryptedToken !== '') + ? trim($decryptedToken) + : $rawPassengerToken; + } + } catch (PDOException $eTok) { + error_log("[accept_ride] passenger token lookup WARNING: " . $eTok->getMessage()); + } + } + if (empty($passengerToken)) { + $passengerToken = $passengerTokenFromClient; + } + if ($passengerIdValue) { // Socket — real-time update على خريطة الراكب if (function_exists('notifyPassengerOnRideServer')) { diff --git a/backend/ride/rides/add_ride.php b/backend/ride/rides/add_ride.php index a43f365e..b5b89b4f 100644 --- a/backend/ride/rides/add_ride.php +++ b/backend/ride/rides/add_ride.php @@ -24,19 +24,29 @@ function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketDa } } $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; + // ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي + // بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل + // الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php. + // + // أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر + // رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص + // يصل للسيرفر لاحقاً كنص "null". $marketPayload = array_merge([ - 'id' => (string)$rideId, - 'start_lat' => $lat, - 'start_lng' => $lng, - 'end_lat' => $payloadData[3], - 'end_lng' => $payloadData[4], - 'price' => $payloadData[2], - 'carType' => $payloadData[31], - 'startName' => $payloadData[29], - 'endName' => $payloadData[30], - 'distance' => $payloadData[11], - 'duration' => $payloadData[15], - 'passengerRate' => $payloadData[33], + 'id' => (string)$rideId, + 'start_lat' => $lat, + 'start_lng' => $lng, + 'end_lat' => $payloadData[3], + 'end_lng' => $payloadData[4], + 'start_location' => $lat . ',' . $lng, + 'end_location' => $payloadData[3] . ',' . $payloadData[4], + 'price' => $payloadData[2], + 'carType' => $payloadData[31], + 'startName' => $payloadData[29], + 'endName' => $payloadData[30], + 'distance' => $payloadData[11], + 'duration' => $payloadData[15], + 'passengerRate' => $payloadData[33], + 'passengerId' => $payloadData[7], ], $extraMarketData); $postData = [ diff --git a/backend/ride/rides/cancel_ride_by_passenger.php b/backend/ride/rides/cancel_ride_by_passenger.php index e1f26f8e..08cd855d 100644 --- a/backend/ride/rides/cancel_ride_by_passenger.php +++ b/backend/ride/rides/cancel_ride_by_passenger.php @@ -82,7 +82,7 @@ try { // 🔥 يُرسل دائماً بغض النظر عن driver_id — إذا كانت الرحلة لم تُقبل بعد // (driver_id = 0)، لوكيشن سيرفر يستخدم ride:offered_drivers:{rideId} // من Redis لإشعار كل السائقين الذين وصلهم عرض هذه الرحلة أصلاً. - $socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://nginx/loction_server/driver_socket.php'; + $socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021'; $internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: ''; $internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: ''); diff --git a/docker/.env.example b/docker/.env.example index 3f0b7772..602fcf7b 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -57,6 +57,9 @@ REDIS_MAIN_PASSWORD= # --- 4. Internal Sockets (Docker Network Names) --- LOCATION_SERVER_URL=http://socket_driver:2021 RIDE_SOCKET_URL=http://socket_passenger:3031 +# ⚠️ إجباري في Docker: بدونه يستخدم driver_socket.php الافتراضي 127.0.0.1:3031 +# وهو داخل حاوية socket_driver يشير لنفسها، فلا يصل الراكب أي تحديث موقع. +PASSENGER_SOCKET_INTERNAL_URL=http://socket_passenger:3031 # --- 5. External Sockets & URLs --- LOCATION_SOCKET_URL=http://socket_driver:2021 @@ -135,7 +138,10 @@ SIRO_COMMISSION_IQ=0.14 ADMIN_TOOL_IPS=92.241.36.211,92.241.36.165,92.241.37.3 ALLOWED_UPLOAD_HOST=siromove.com ALLOWED_TESTER_EMAILS=driver_tester@siromove.com,passenger_tester@siromove.com -ALLOWED_SOCKET_URLS=http://188.68.36.205:2021,http://188.68.36.205:3031,https://location.intaleq.xyz +# ⚠️ لازم يغطّي كل قيمة في القسم 4/5 أعلاه: isAllowedSocketUrl() تعمل +# str_starts_with، وأي عنوان غير مغطّى تُسقَط رسائله (بلا أي أثر قبل هذا الإصلاح). +# أسماء حاويات Docker إجبارية هنا — لا تحذفها واستبدلها بالـ IP فقط. +ALLOWED_SOCKET_URLS=http://socket_driver,http://socket_passenger,http://nginx,http://188.68.36.205:2021,http://188.68.36.205:3031,https://location.intaleq.xyz ALLOWED_ORIGIN=https://siromove.com ADMIN_PHONE_NUMBERS=963992952235,963942542053,962772735902 SERVICE_PHONE1=963952475740 diff --git a/loction_server/driver_socket.php b/loction_server/driver_socket.php index 23acf4fc..86c46d4e 100755 --- a/loction_server/driver_socket.php +++ b/loction_server/driver_socket.php @@ -171,19 +171,22 @@ function forwardLocationToPassengerSocket( $now = time(); $last = $fwdThrottle[$driverId] ?? null; + // ⚠️ الحمولة تستخدم latitude/longitude (انظر update_location) — قراءة + // lat/lng هنا كانت ترجع null فتصير المسافة ضخمة، فلا يعمل الـ throttle + // إطلاقاً ويُعاد التوجيه مع كل نبضة GPS. + $curLat = (float)($payload['latitude'] ?? $payload['lat'] ?? 0); + $curLng = (float)($payload['longitude'] ?? $payload['lng'] ?? 0); + if ($last !== null) { $timeDiff = $now - $last['ts']; - $dist = haversineDistance( - $last['lat'], $last['lng'], - (float)$payload['lat'], (float)$payload['lng'] - ); + $dist = haversineDistance($last['lat'], $last['lng'], $curLat, $curLng); if ($dist < FORWARD_MIN_METERS && $timeDiff < FORWARD_MAX_SECONDS) return; } $fwdThrottle[$driverId] = [ 'ts' => $now, - 'lat' => (float)$payload['lat'], - 'lng' => (float)$payload['lng'], + 'lat' => $curLat, + 'lng' => $curLng, ]; $passengerSocketUrl = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: 'http://127.0.0.1:3031'; diff --git a/ride_server/passenger_socket.php b/ride_server/passenger_socket.php index 0e6bf4ce..4d5d4992 100755 --- a/ride_server/passenger_socket.php +++ b/ride_server/passenger_socket.php @@ -63,6 +63,34 @@ function getInternalSocketKey(): string { $INTERNAL_KEY = getInternalSocketKey(); +// ── Redis سيرفر الموقع (للتحقق من عضوية الراكب في خط مواصلاتي) ── +// نفس الـ Redis الذي يكتب عليه driver_socket و transit/functions.php. +// الهوست من REDIS_HOST حتى يعمل داخل Docker وخارجه على حدّ سواء. +$_locationRedis = null; + +function getLocationRedis(): ?\Redis { + global $_locationRedis; + if ($_locationRedis !== null) { + try { $_locationRedis->ping(); return $_locationRedis; } + catch (\Exception $_) { $_locationRedis = null; } + } + try { + $redisPass = ''; + foreach ([getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'] as $p) { + if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; } + } + $host = getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1'); + $r = new \Redis(); + $r->connect($host, (int)(getenv('REDIS_PORT') ?: 6379), 1.5); + if ($redisPass) $r->auth($redisPass); + $_locationRedis = $r; + return $r; + } catch (\Exception $e) { + socket_log("[REDIS_ERROR] Location Redis unavailable: " . $e->getMessage()); + return null; + } +} + function getJwtSecret(): string { $keyPath = getenv('JWT_SECRET_KEY_PATH'); if ($keyPath && file_exists($keyPath)) { @@ -142,6 +170,28 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY, $INTERNAL_PORT) { $connection->send('OK'); + } elseif ($action === 'broadcast_bus_location') { + + // 🚌 بثّ موقع الباص لكل ركاب الخط المشتركين (مواصلاتي) + // يصل من driver_socket بعد أن يبعث سائق الباص update_bus_location + $routeId = $post['route_id'] ?? null; + $rawPayload = $post['payload'] ?? null; + + if (!$routeId || !$rawPayload) { + socket_log("[HTTP_ERROR] Missing route_id or payload for action: broadcast_bus_location", $post); + $connection->send('Error: Missing route_id or payload'); + return; + } + + $payload = is_string($rawPayload) + ? (json_decode($rawPayload, true) ?? $rawPayload) + : $rawPayload; + + socket_log("[HTTP_SUCCESS] Emitting 'bus_location_update' to route #$routeId", $payload); + $io->to('transit_route_' . $routeId)->emit('bus_location_update', $payload); + + $connection->send('OK'); + } else { socket_log("[HTTP_WARNING] Unknown action received: $action", $post); $connection->send('Unknown action: ' . $action); @@ -191,6 +241,46 @@ $io->on('connection', function ($socket) { // socket_log("[SOCKET_HEARTBEAT] Received from Passenger #$passengerId"); }); + // 🚌 اشتراك الراكب في بثّ موقع باص خط (مواصلاتي) + // يُرفض إن لم يكن الراكب عضواً نشطاً في المؤسسة المالكة للخط + $socket->on('subscribe_transit_route', function ($data) use ($socket, $passengerId) { + $data = (array) $data; + $routeId = (int)($data['route_id'] ?? 0); + if ($routeId <= 0) return; + + // transit:route_org:{routeId} → org_id (كُتب في route/approve.php) + // transit:org_members:{orgId} → SET من passenger_ids (transitCacheEnrollment) + $redis = getLocationRedis(); + if ($redis) { + $orgId = $redis->get("transit:route_org:{$routeId}"); + if ($orgId === false || $orgId === null) { + socket_log("[TRANSIT_BLOCKED] Route #{$routeId} not in Redis — passenger #{$passengerId}"); + $socket->emit('transit_error', ['code' => 'ROUTE_NOT_FOUND', 'route_id' => $routeId]); + return; + } + if (!$redis->sIsMember("transit:org_members:{$orgId}", (string)$passengerId)) { + socket_log("[TRANSIT_BLOCKED] Passenger #{$passengerId} not enrolled in org #{$orgId} (route #{$routeId})"); + $socket->emit('transit_error', ['code' => 'NOT_ENROLLED', 'route_id' => $routeId]); + return; + } + } else { + socket_log("[TRANSIT_WARN] Redis unavailable — enrollment check skipped (passenger #{$passengerId} route #{$routeId})"); + } + + $socket->join('transit_route_' . $routeId); + socket_log("[TRANSIT] Passenger #$passengerId subscribed to route #$routeId"); + }); + + // 🚌 إلغاء الاشتراك عند إغلاق الراكب للخط + $socket->on('unsubscribe_transit_route', function ($data) use ($socket, $passengerId) { + $data = (array) $data; + $routeId = (int)($data['route_id'] ?? 0); + if ($routeId <= 0) return; + + $socket->leave('transit_route_' . $routeId); + socket_log("[TRANSIT] Passenger #$passengerId unsubscribed from route #$routeId"); + }); + $socket->on('disconnect', function () use ($passengerId, $clientIp) { socket_log("[SOCKET_DISCONNECTED] Passenger Disconnected: #$passengerId (IP: $clientIp)"); }); diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart index 3a442ce6..e4212bff 100755 --- a/siro_driver/lib/controller/firebase/firbase_messge.dart +++ b/siro_driver/lib/controller/firebase/firbase_messge.dart @@ -126,9 +126,11 @@ class FirebaseMessagesController extends GetxController { // اقرأ "النوع" من حمولة البيانات، وليس من العنوان String category = message.data['category'] ?? ''; - // اقرأ العنوان والنص (للعرض) - String title = message.notification?.title ?? ''; - String body = message.notification?.body ?? ''; + // اقرأ العنوان والنص (للعرض) — data أولاً ثم notification، مثل تطبيق الراكب. + // السيرفر (FcmService/send_fcm.php) يحقن title/body داخل data، والرسائل + // الصامتة (data-only) بلا notification أصلاً فكانا يخرجان فارغين. + String title = message.data['title'] ?? message.notification?.title ?? ''; + String body = message.data['body'] ?? message.notification?.body ?? ''; // استخدم switch لسهولة القراءة والصيانة switch (category) { @@ -295,8 +297,8 @@ class FirebaseMessagesController extends GetxController { default: Log.print('Received unhandled notification category: $category'); - // Optionally show a generic notification - if (Platform.isAndroid) { + // إشعار عام — لكن لا نعرض فقاعة فارغة للرسائل الصامتة (data-only) + if (Platform.isAndroid && (title.isNotEmpty || body.isNotEmpty)) { notificationController.showNotification(title, body, 'default', ''); } break; diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart index bf8f9b22..3f090f88 100644 --- a/siro_rider/lib/controller/home/map/map_socket_controller.dart +++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart @@ -6,8 +6,10 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; -import '../../../main.dart'; // contains global 'box' +import '../../../env/env.dart'; +import '../../../main.dart'; // contains global 'box' + 'storage' import '../../../print.dart'; +import '../../functions/encrypt_decrypt.dart'; // r() import 'ride_lifecycle_controller.dart'; import 'nearby_drivers_controller.dart'; import 'map_engine_controller.dart'; @@ -29,10 +31,38 @@ class MapSocketController extends GetxController { DateTime? get lastDriverLocationTime => _lastSocketLocationTime; int get socketLocationUpdatesCount => _socketLocationUpdatesCount; - void initConnectionWithSocket() { + /// يقرأ الـ JWT بنفس آلية CRUD._getJwt (SecureStorage أولاً ثم GetStorage). + /// passenger_socket.php يرفض أي اتصال بلا jwt، فبدونه لا تصل أي أحداث للراكب. + Future _getJwtForSocket() async { + try { + final String? encryptedJwt = await storage.read(key: BoxName.jwt); + if (encryptedJwt != null && encryptedJwt.isNotEmpty) { + return r(encryptedJwt).toString().split(Env.addd)[0]; + } + } catch (e) { + Log.print('Error reading JWT from SecureStorage for socket: $e'); + } + final String? fallback = box.read(BoxName.jwt); + if (fallback != null && fallback.toString().isNotEmpty) { + try { + return r(fallback).toString().split(Env.addd)[0]; + } catch (e) { + Log.print('Error decrypting fallback JWT for socket: $e'); + } + } + return ''; + } + + Future initConnectionWithSocket() async { if (isSocketConnected) return; String passengerId = box.read(BoxName.passengerID).toString(); + final String jwt = await _getJwtForSocket(); + if (jwt.isEmpty) { + Log.print( + "⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect."); + return; + } Log.print("🔌 Initializing Socket for Passenger: $passengerId"); socket = io_client.io( @@ -40,7 +70,7 @@ class MapSocketController extends GetxController { io_client.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() - .setQuery({'id': passengerId}) + .setQuery({'id': passengerId, 'jwt': jwt}) .setReconnectionAttempts(20) .setReconnectionDelay(2000) .setReconnectionDelayMax(10000) @@ -171,13 +201,15 @@ class MapSocketController extends GetxController { // ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ───────────── // يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً. - void subscribeToTransitRoute(int routeId) { + Future subscribeToTransitRoute(int routeId) async { _subscribedTransitRouteId = routeId; if (!isSocketConnected) { - initConnectionWithSocket(); + await initConnectionWithSocket(); // سيُعاد الاشتراك تلقائياً من onConnect إن أضفنا ذلك، لكن نحاول فوراً أيضاً } - if (socket.connected) { + // socket هو late — لو انسحب initConnectionWithSocket (بلا JWT مثلاً) يبقى + // غير مُهيّأ، فقراءته مباشرة ترمي LateInitializationError. + if (_isSocketInitialized && socket.connected) { socket.emit('subscribe_transit_route', {'route_id': routeId}); Log.print('🚌 Subscribed to transit route #$routeId'); } @@ -185,7 +217,7 @@ class MapSocketController extends GetxController { void unsubscribeFromTransitRoute(int routeId) { if (_subscribedTransitRouteId == routeId) _subscribedTransitRouteId = null; - if (isSocketConnected && socket.connected) { + if (_isSocketInitialized && isSocketConnected && socket.connected) { socket.emit('unsubscribe_transit_route', {'route_id': routeId}); Log.print('🚌 Unsubscribed from transit route #$routeId'); } @@ -243,7 +275,12 @@ class MapSocketController extends GetxController { _onRideFinishedWithSocket(data); break; + // cancel_ride_by_driver.php يبعث 'cancelled_by_driver' حرفياً، + // و cancel_ride_by_passenger.php يبعث 'cancelled_by_passenger'. + // كانت الحالتان تسقطان من الـ switch فيبقى الراكب معلّقاً على الشاشة. case 'cancelled': + case 'cancelled_by_driver': + case 'canceled_by_driver': rideLifecycle.processRideCancelledByDriver(data, source: "Socket"); break;