// turn_credentials_service.dart — جلب بيانات اعتماد TURN المؤقتة وتخزينها // // خادم الإشارات (Node) يُعيد STUN فقط، وSTUN وحده لا يكفي حين يكون الطرفان // خلف CGNAT — وهي الحالة الغالبة لسائق وزبون على بيانات الجوال. نجلب TURN // من الباك إند ونضمّه إلى قائمة ICE قبل إنشاء الاتصال. // // البيانات مؤقتة (HMAC بمهلة) لا كلمة مرور ثابتة داخل التطبيق، ونُخزّنها // محلياً حتى قرب انتهائها فلا نُثقل بنداء شبكة قبل كل مكالمة. import 'dart:convert'; import '../constant/box_name.dart'; import '../constant/links.dart'; import '../controller/functions/crud.dart'; import '../main.dart'; import '../print.dart'; class TurnCredentialsService { static const String _cacheKey = BoxName.turnIceCache; /// خوادم ICE جاهزة لتمريرها إلى createPeerConnection. /// تعيد قائمة فارغة عند أي فشل — والمُنادي يكمل بـ STUN كما كان سابقاً. static Future>> getIceServers() async { final cached = _readCache(); if (cached != null) return cached; try { final res = await CRUD().post( link: '${AppLink.server}/ride/call/turn_credentials.php', ); if (res is! Map || res['status'] != 'success') return []; // غلاف jsonSuccess يضع الحمولة في message final payload = res['message']; if (payload is! Map || payload['ice_servers'] is! List) return []; final servers = (payload['ice_servers'] as List) .whereType() .map((s) => Map.from(s)) .toList(); final ttl = int.tryParse(payload['ttl']?.toString() ?? '0') ?? 0; if (ttl > 0 && servers.isNotEmpty) { // ننتهي قبل الخادم بخمس دقائق تفادياً لسباق انتهاء الصلاحية أثناء مكالمة final expiresAt = DateTime.now().add(Duration(seconds: ttl - 300)); box.write(_cacheKey, jsonEncode({ 'expires_at': expiresAt.toIso8601String(), 'servers': servers, })); } return servers; } catch (e) { Log.print('⚠️ [TURN] تعذّر جلب بيانات الاعتماد: $e'); return []; } } static List>? _readCache() { try { final raw = box.read(_cacheKey); if (raw == null) return null; final data = jsonDecode(raw.toString()); final expiresAt = DateTime.tryParse(data['expires_at']?.toString() ?? ''); if (expiresAt == null || DateTime.now().isAfter(expiresAt)) return null; final servers = (data['servers'] as List) .whereType() .map((s) => Map.from(s)) .toList(); return servers.isEmpty ? null : servers; } catch (_) { return null; } } static void clearCache() => box.remove(_cacheKey); }