library api_service; import 'dart:async'; import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import 'package:gwid/connection/connection_logger.dart'; import 'package:gwid/connection/connection_state.dart' as conn_state; import 'package:gwid/connection/health_monitor.dart'; import 'package:gwid/image_cache_service.dart'; import 'package:gwid/models/complaint.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/profile.dart'; import 'package:gwid/proxy_service.dart'; import 'package:gwid/services/account_manager.dart'; import 'package:gwid/services/avatar_cache_service.dart'; import 'package:gwid/services/cache_service.dart'; import 'package:gwid/services/chat_cache_service.dart'; import 'package:gwid/services/profile_cache_service.dart'; import 'package:gwid/spoofing_service.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/status.dart' as status; part 'api_service_connection.dart'; part 'api_service_auth.dart'; part 'api_service_contacts.dart'; part 'api_service_chats.dart'; part 'api_service_media.dart'; part 'api_service_privacy.dart'; part 'api_service_complaints.dart'; class ApiService { ApiService._privateConstructor(); static final ApiService instance = ApiService._privateConstructor(); int? _userId; late int _sessionId; int _actionId = 1; bool _isColdStartSent = false; late int _lastActionTime; bool _isAppInForeground = true; final List _wsUrls = ['wss://ws-api.oneme.ru:443/websocket']; int _currentUrlIndex = 0; List get wsUrls => _wsUrls; int get currentUrlIndex => _currentUrlIndex; IOWebSocketChannel? _channel; StreamSubscription? _streamSubscription; Timer? _pingTimer; int _seq = 0; final StreamController _contactUpdatesController = StreamController.broadcast(); Stream get contactUpdates => _contactUpdatesController.stream; final StreamController _errorController = StreamController.broadcast(); Stream get errorStream => _errorController.stream; final _reconnectionCompleteController = StreamController.broadcast(); Stream get reconnectionComplete => _reconnectionCompleteController.stream; final Map _presenceData = {}; String? authToken; String? userId; String? get token => authToken; String? _currentPasswordTrackId; String? _currentPasswordHint; String? _currentPasswordEmail; bool _isSessionOnline = false; bool _handshakeSent = false; Completer? _onlineCompleter; final List> _messageQueue = []; final Map> _messageCache = {}; final Map _contactCache = {}; DateTime? _lastContactsUpdate; static const Duration _contactCacheExpiry = Duration(minutes: 5); final CacheService _cacheService = CacheService(); final AvatarCacheService _avatarCacheService = AvatarCacheService(); final ChatCacheService _chatCacheService = ChatCacheService(); bool _cacheServicesInitialized = false; final ConnectionLogger _connectionLogger = ConnectionLogger(); final conn_state.ConnectionStateManager _connectionStateManager = conn_state.ConnectionStateManager(); final HealthMonitor _healthMonitor = HealthMonitor(); String? _currentServerUrl; bool _isLoadingBlockedContacts = false; bool _isSessionReady = false; final _messageController = StreamController>.broadcast(); Stream> get messages => _messageController.stream; final _connectionStatusController = StreamController.broadcast(); Stream get connectionStatus => _connectionStatusController.stream; final _connectionLogController = StreamController.broadcast(); Stream get connectionLog => _connectionLogController.stream; List get logs => _connectionLogger.logs; Stream get connectionState => _connectionStateManager.stateStream; Stream get healthMetrics => _healthMonitor.metricsStream; final List _connectionLogCache = []; List get connectionLogCache => _connectionLogCache; bool get isOnline => _isSessionOnline; Future waitUntilOnline() async { if (_isSessionOnline && _isSessionReady) return; _onlineCompleter ??= Completer(); return _onlineCompleter!.future; } bool get isActuallyConnected { try { if (_channel == null || !_isSessionOnline) { return false; } return true; } catch (e) { print("🔴 Ошибка при проверке состояния канала: $e"); return false; } } Completer>? _inflightChatsCompleter; Map? _lastChatsPayload; DateTime? _lastChatsAt; final Duration _chatsCacheTtl = const Duration(seconds: 5); bool _chatsFetchedInThisSession = false; Map? get lastChatsPayload => _lastChatsPayload; int _reconnectDelaySeconds = 2; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 10; Timer? _reconnectTimer; bool _isReconnecting = false; void _log( String message, { LogLevel level = LogLevel.info, String category = 'API', Map? data, }) { print(message); _connectionLogCache.add(message); if (!_connectionLogController.isClosed) { _connectionLogController.add(message); } _connectionLogger.log( message, level: level, category: category, data: data, ); } void _emitLocal(Map frame) { try { _messageController.add(frame); } catch (_) {} } String generateRandomDeviceId() { return const Uuid().v4(); } Future> _buildUserAgentPayload() async { final spoofedData = await SpoofingService.getSpoofedSessionData(); if (spoofedData != null) { print( '--- [_buildUserAgentPayload] Используются подменённые данные сессии ---', ); final String finalDeviceId; final String? idFromSpoofing = spoofedData['device_id'] as String?; if (idFromSpoofing != null && idFromSpoofing.isNotEmpty) { finalDeviceId = idFromSpoofing; print('Используется deviceId из сессии: $finalDeviceId'); } else { finalDeviceId = generateRandomDeviceId(); print('device_id не найден в кэше, сгенерирован новый: $finalDeviceId'); } return { 'deviceType': spoofedData['device_type'] as String? ?? 'IOS', 'locale': spoofedData['locale'] as String? ?? 'ru', 'deviceLocale': spoofedData['locale'] as String? ?? 'ru', 'osVersion': spoofedData['os_version'] as String? ?? 'iOS 17.5.1', 'deviceName': spoofedData['device_name'] as String? ?? 'iPhone', 'headerUserAgent': spoofedData['user_agent'] as String? ?? 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1', 'appVersion': spoofedData['app_version'] as String? ?? '25.10.10', 'screen': spoofedData['screen'] as String? ?? '1170x2532 3.0x', 'timezone': spoofedData['timezone'] as String? ?? 'Europe/Moscow', }; } else { print( '--- [_buildUserAgentPayload] Используются псевдо-случайные данные ---', ); return { 'deviceType': 'WEB', 'locale': 'ru', 'deviceLocale': 'ru', 'osVersion': 'Windows', 'deviceName': 'Chrome', 'headerUserAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'appVersion': '25.10.10', 'screen': '1920x1080 1.0x', 'timezone': 'Europe/Moscow', }; } } void setAppInForeground(bool isForeground) { _isAppInForeground = isForeground; } void _updateConnectionState( conn_state.ConnectionState state, { String? message, int? attemptNumber, Duration? reconnectDelay, int? latency, Map? metadata, }) { _connectionStateManager.setState( state, message: message, attemptNumber: attemptNumber, reconnectDelay: reconnectDelay, serverUrl: _currentServerUrl, latency: latency, metadata: metadata, ); } void _startHealthMonitoring() { _healthMonitor.startMonitoring(serverUrl: _currentServerUrl); } void _stopHealthMonitoring() { _healthMonitor.stopMonitoring(); } Future initialize() async { await _ensureCacheServicesInitialized(); } Future _ensureCacheServicesInitialized() async { if (_cacheServicesInitialized) return; await Future.wait([ _cacheService.initialize(), _avatarCacheService.initialize(), _chatCacheService.initialize(), ImageCacheService.instance.initialize(), ]); _cacheServicesInitialized = true; } Future getClipboardData() async { final data = await Clipboard.getData(Clipboard.kTextPlain); return data?.text; } void dispose() { _pingTimer?.cancel(); _channel?.sink.close(status.goingAway); _reconnectionCompleteController.close(); _messageController.close(); } }