import 'dart:async'; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_socket_channel/io.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/contact.dart'; import 'package:web_socket_channel/status.dart' as status; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:gwid/spoofing_service.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter/services.dart'; import 'package:gwid/proxy_service.dart'; import 'package:file_picker/file_picker.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, ); // Кэш на 5 минут 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; final List _connectionLogCache = []; List get connectionLogCache => _connectionLogCache; void _log(String message) { print(message); // Оставляем для дебага в консоли _connectionLogCache.add(message); if (!_connectionLogController.isClosed) { _connectionLogController.add(message); } } void _emitLocal(Map frame) { try { _messageController.add(frame); } catch (_) {} } 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; Future _connectWithFallback() async { _log('Начало подключения...'); while (_currentUrlIndex < _wsUrls.length) { final currentUrl = _wsUrls[_currentUrlIndex]; final logMessage = 'Попытка ${_currentUrlIndex + 1}/${_wsUrls.length}: $currentUrl'; _log(logMessage); _connectionLogController.add(logMessage); try { await _connectToUrl(currentUrl); final successMessage = _currentUrlIndex == 0 ? 'Подключено к основному серверу' : 'Подключено через резервный сервер'; _connectionLogController.add('✅ $successMessage'); if (_currentUrlIndex > 0) { _connectionStatusController.add('Подключено через резервный сервер'); } return; // Успешно подключились } catch (e) { final errorMessage = '❌ Ошибка: ${e.toString().split(':').first}'; print('Ошибка подключения к $currentUrl: $e'); _connectionLogController.add(errorMessage); _currentUrlIndex++; if (_currentUrlIndex < _wsUrls.length) { await Future.delayed(const Duration(milliseconds: 500)); } } } _log('❌ Все серверы недоступны'); _connectionStatusController.add('Все серверы недоступны'); throw Exception('Не удалось подключиться ни к одному серверу'); } Future _connectToUrl(String url) async { _isSessionOnline = false; _onlineCompleter = Completer(); final bool hadChatsFetched = _chatsFetchedInThisSession; final bool hasValidToken = authToken != null; if (!hasValidToken) { _chatsFetchedInThisSession = false; } else { _chatsFetchedInThisSession = hadChatsFetched; } _connectionStatusController.add('connecting'); final uri = Uri.parse(url); print( 'Parsed URI: host=${uri.host}, port=${uri.port}, scheme=${uri.scheme}', ); final spoofedData = await SpoofingService.getSpoofedSessionData(); final userAgent = spoofedData?['useragent'] as String? ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; final headers = { 'Origin': 'https://web.max.ru', 'User-Agent': userAgent, 'Sec-WebSocket-Extensions': 'permessage-deflate', }; final proxySettings = await ProxyService.instance.loadProxySettings(); if (proxySettings.isEnabled && proxySettings.host.isNotEmpty) { print( 'Используем ${proxySettings.protocol.name.toUpperCase()} прокси ${proxySettings.host}:${proxySettings.port}', ); final customHttpClient = await ProxyService.instance .getHttpClientWithProxy(); _channel = IOWebSocketChannel.connect( uri, headers: headers, customClient: customHttpClient, ); } else { print('Подключение без прокси'); _channel = IOWebSocketChannel.connect(uri, headers: headers); } await _channel!.ready; _listen(); await _sendHandshake(); _startPinging(); } int _reconnectDelaySeconds = 2; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 10; Timer? _reconnectTimer; bool _isReconnecting = false; 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 _handleSessionTerminated() { print("Сессия была завершена сервером"); _isSessionOnline = false; _isSessionReady = false; authToken = null; clearAllCaches(); _messageController.add({ 'type': 'session_terminated', 'message': 'Твоя сессия больше не активна, войди снова', }); } void _handleInvalidToken() async { print("Обработка недействительного токена"); _isSessionOnline = false; _isSessionReady = false; authToken = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove('authToken'); clearAllCaches(); _channel?.sink.close(); _channel = null; _pingTimer?.cancel(); _messageController.add({ 'type': 'invalid_token', 'message': 'Токен недействителен, требуется повторная авторизация', }); } Future _clearAuthToken() async { print("Очищаем токен авторизации..."); authToken = null; _lastChatsPayload = null; _lastChatsAt = null; _chatsFetchedInThisSession = false; final prefs = await SharedPreferences.getInstance(); await prefs.remove('authToken'); clearAllCaches(); _connectionStatusController.add("disconnected"); } Future _sendHandshake() async { if (_handshakeSent) { print('Handshake уже отправлен, пропускаем...'); return; } print('Отправляем handshake...'); final userAgentPayload = await _buildUserAgentPayload(); final prefs = await SharedPreferences.getInstance(); final deviceId = prefs.getString('spoof_deviceid') ?? generateRandomDeviceId(); if (prefs.getString('spoof_deviceid') == null) { await prefs.setString('spoof_deviceid', deviceId); } final payload = {'deviceId': deviceId, 'userAgent': userAgentPayload}; print('Отправляем handshake с payload: $payload'); _sendMessage(6, payload); _handshakeSent = true; print('Handshake отправлен, ожидаем ответ...'); } Future requestOtp(String phoneNumber) async { if (_channel == null) { print('WebSocket не подключен, подключаемся...'); try { await connect(); await waitUntilOnline(); } catch (e) { print('Ошибка подключения к WebSocket: $e'); throw Exception('Не удалось подключиться к серверу: $e'); } } final payload = { "phone": phoneNumber, "type": "START_AUTH", "language": "ru", }; _sendMessage(17, payload); } void requestSessions() { _sendMessage(96, {}); } void terminateAllSessions() { _sendMessage(97, {}); } Future blockContact(int contactId) async { await waitUntilOnline(); _sendMessage(34, {'contactId': contactId, 'action': 'BLOCK'}); } Future unblockContact(int contactId) async { await waitUntilOnline(); _sendMessage(34, {'contactId': contactId, 'action': 'UNBLOCK'}); } Future addContact(int contactId) async { await waitUntilOnline(); _sendMessage(34, {'contactId': contactId, 'action': 'ADD'}); } Future subscribeToChat(int chatId, bool subscribe) async { await waitUntilOnline(); _sendMessage(75, {'chatId': chatId, 'subscribe': subscribe}); } Future navigateToChat(int currentChatId, int targetChatId) async { await waitUntilOnline(); if (currentChatId != 0) { await subscribeToChat(currentChatId, false); } await subscribeToChat(targetChatId, true); } Future clearChatHistory(int chatId, {bool forAll = false}) async { await waitUntilOnline(); final payload = { 'chatId': chatId, 'forAll': forAll, 'lastEventTime': DateTime.now().millisecondsSinceEpoch, }; _sendMessage(54, payload); } Future> getChatInfoByLink(String link) async { await waitUntilOnline(); final payload = {'link': link}; final int seq = _sendMessage(89, payload); print('Запрашиваем информацию о чате (seq: $seq) по ссылке: $link'); try { final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 10)); if (response['cmd'] == 3) { final errorPayload = response['payload'] ?? {}; final errorMessage = errorPayload['localizedMessage'] ?? errorPayload['message'] ?? 'Неизвестная ошибка'; print('Ошибка получения информации о чате: $errorMessage'); throw Exception(errorMessage); } if (response['cmd'] == 1 && response['payload'] != null && response['payload']['chat'] != null) { print( 'Информация о чате получена: ${response['payload']['chat']['title']}', ); return response['payload']['chat'] as Map; } else { print('Не удалось найти "chat" в ответе opcode 89: $response'); throw Exception('Неверный ответ от сервера'); } } on TimeoutException { print('Таймаут ожидания ответа на getChatInfoByLink (seq: $seq)'); throw Exception('Сервер не ответил вовремя'); } catch (e) { print('Ошибка в getChatInfoByLink: $e'); rethrow; } } void markMessageAsRead(int chatId, String messageId) { waitUntilOnline().then((_) { final payload = { "type": "READ_MESSAGE", "chatId": chatId, "messageId": messageId, "mark": DateTime.now().millisecondsSinceEpoch, }; _sendMessage(50, payload); print( 'Отправляем отметку о прочтении для сообщения $messageId в чате $chatId', ); }); } void getBlockedContacts() async { if (_isLoadingBlockedContacts) { print( 'ApiService: запрос заблокированных контактов уже выполняется, пропускаем', ); return; } _isLoadingBlockedContacts = true; print('ApiService: запрашиваем заблокированные контакты'); _sendMessage(36, {'status': 'BLOCKED', 'count': 100, 'from': 0}); Future.delayed(const Duration(seconds: 2), () { _isLoadingBlockedContacts = false; }); } void notifyContactUpdate(Contact contact) { print( 'ApiService отправляет обновление контакта: ${contact.name} (ID: ${contact.id}), isBlocked: ${contact.isBlocked}, isBlockedByMe: ${contact.isBlockedByMe}', ); _contactUpdatesController.add(contact); } DateTime? getLastSeen(int userId) { final userPresence = _presenceData[userId.toString()]; if (userPresence != null && userPresence['seen'] != null) { final seenTimestamp = userPresence['seen'] as int; return DateTime.fromMillisecondsSinceEpoch(seenTimestamp * 1000); } return null; } void updatePresenceData(Map presenceData) { _presenceData.addAll(presenceData); print('ApiService обновил presence данные: $_presenceData'); } void sendReaction(int chatId, String messageId, String emoji) { final payload = { "chatId": chatId, "messageId": messageId, "reaction": {"reactionType": "EMOJI", "id": emoji}, }; _sendMessage(178, payload); print('Отправляем реакцию: $emoji на сообщение $messageId в чате $chatId'); } void removeReaction(int chatId, String messageId) { final payload = {"chatId": chatId, "messageId": messageId}; _sendMessage(179, payload); print('Удаляем реакцию с сообщения $messageId в чате $chatId'); } void createGroup(String name, List participantIds) { final payload = {"name": name, "participantIds": participantIds}; _sendMessage(48, payload); print('Создаем группу: $name с участниками: $participantIds'); } void updateGroup(int chatId, {String? name, List? participantIds}) { final payload = { "chatId": chatId, if (name != null) "name": name, if (participantIds != null) "participantIds": participantIds, }; _sendMessage(272, payload); print('Обновляем группу $chatId: $payload'); } void createGroupWithMessage(String name, List participantIds) { final cid = DateTime.now().millisecondsSinceEpoch; final payload = { "message": { "cid": cid, "attaches": [ { "_type": "CONTROL", "event": "new", "chatType": "CHAT", "title": name, "userIds": participantIds, }, ], }, "notify": true, }; _sendMessage(64, payload); print('Создаем группу: $name с участниками: $participantIds'); } void renameGroup(int chatId, String newName) { final payload = {"chatId": chatId, "theme": newName}; _sendMessage(55, payload); print('Переименовываем группу $chatId в: $newName'); } void addGroupMember( int chatId, List userIds, { bool showHistory = true, }) { final payload = { "chatId": chatId, "userIds": userIds, "showHistory": showHistory, "operation": "add", }; _sendMessage(77, payload); print('Добавляем участников $userIds в группу $chatId'); } void removeGroupMember( int chatId, List userIds, { int cleanMsgPeriod = 0, }) { final payload = { "chatId": chatId, "userIds": userIds, "operation": "remove", "cleanMsgPeriod": cleanMsgPeriod, }; _sendMessage(77, payload); print('Удаляем участников $userIds из группы $chatId'); } void leaveGroup(int chatId) { final payload = {"chatId": chatId}; _sendMessage(58, payload); print('Выходим из группы $chatId'); } void getGroupMembers(int chatId, {int marker = 0, int count = 50}) { final payload = { "type": "MEMBER", "marker": marker, "chatId": chatId, "count": count, }; _sendMessage(59, payload); print( 'Запрашиваем участников группы $chatId (marker: $marker, count: $count)', ); } Future getChatIdByUserId(int userId) async { await waitUntilOnline(); final payload = { "chatIds": [userId], }; final int seq = _sendMessage(48, payload); print('Запрашиваем информацию о чате для userId: $userId (seq: $seq)'); try { final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 10)); if (response['cmd'] == 3) { final errorPayload = response['payload'] ?? {}; final errorMessage = errorPayload['localizedMessage'] ?? errorPayload['message'] ?? 'Неизвестная ошибка'; print('Ошибка получения информации о чате: $errorMessage'); return null; } if (response['cmd'] == 1 && response['payload'] != null) { final chats = response['payload']['chats'] as List?; if (chats != null && chats.isNotEmpty) { final chat = chats[0] as Map; final chatId = chat['id'] as int?; final chatType = chat['type'] as String?; if (chatType == 'DIALOG' && chatId != null) { print('Получен chatId для диалога с userId $userId: $chatId'); return chatId; } } } print('Не удалось найти chatId для userId: $userId'); return null; } on TimeoutException { print('Таймаут ожидания ответа на getChatIdByUserId (seq: $seq)'); return null; } catch (e) { print('Ошибка при получении chatId для userId $userId: $e'); return null; } } Future> getChatsOnly({bool force = false}) async { if (authToken == null) { final prefs = await SharedPreferences.getInstance(); authToken = prefs.getString('authToken'); } if (authToken == null) throw Exception("Auth token not found"); if (!force && _lastChatsPayload != null && _lastChatsAt != null) { if (DateTime.now().difference(_lastChatsAt!) < _chatsCacheTtl) { return _lastChatsPayload!; } } try { final payload = {"chatsCount": 100}; final int chatSeq = _sendMessage(48, payload); final chatResponse = await messages.firstWhere( (msg) => msg['seq'] == chatSeq, ); final List chatListJson = chatResponse['payload']?['chats'] ?? []; if (chatListJson.isEmpty) { final result = {'chats': [], 'contacts': [], 'profile': null}; _lastChatsPayload = result; _lastChatsAt = DateTime.now(); return result; } final contactIds = {}; for (var chatJson in chatListJson) { final participants = chatJson['participants'] as Map? ?? {}; contactIds.addAll(participants.keys.map((id) => int.parse(id))); } final int contactSeq = _sendMessage(32, { "contactIds": contactIds.toList(), }); final contactResponse = await messages.firstWhere( (msg) => msg['seq'] == contactSeq, ); final List contactListJson = contactResponse['payload']?['contacts'] ?? []; final result = { 'chats': chatListJson, 'contacts': contactListJson, 'profile': null, 'presence': null, }; _lastChatsPayload = result; final contacts = contactListJson .map((json) => Contact.fromJson(json)) .toList(); updateContactCache(contacts); _lastChatsAt = DateTime.now(); return result; } catch (e) { print('Ошибка получения чатов: $e'); rethrow; } } Future verifyCode(String token, String code) async { _currentPasswordTrackId = null; _currentPasswordHint = null; _currentPasswordEmail = null; if (_channel == null) { print('WebSocket не подключен, подключаемся...'); try { await connect(); await waitUntilOnline(); } catch (e) { print('Ошибка подключения к WebSocket: $e'); throw Exception('Не удалось подключиться к серверу: $e'); } } final payload = { 'token': token, 'verifyCode': code, 'authTokenType': 'CHECK_CODE', }; _sendMessage(18, payload); print('Код верификации отправлен с payload: $payload'); } Future sendPassword(String trackId, String password) async { await waitUntilOnline(); final payload = {'trackId': trackId, 'password': password}; _sendMessage(115, payload); print('Пароль отправлен с payload: $payload'); } Map getPasswordAuthData() { return { 'trackId': _currentPasswordTrackId, 'hint': _currentPasswordHint, 'email': _currentPasswordEmail, }; } void clearPasswordAuthData() { _currentPasswordTrackId = null; _currentPasswordHint = null; _currentPasswordEmail = null; } Future setAccountPassword(String password, String hint) async { await waitUntilOnline(); final payload = {'password': password, 'hint': hint}; _sendMessage(116, payload); print('Запрос на установку пароля отправлен с payload: $payload'); } Future> joinGroupByLink(String link) async { await waitUntilOnline(); final payload = {'link': link}; final int seq = _sendMessage(57, payload); print('Отправляем запрос на присоединение (seq: $seq) по ссылке: $link'); try { final response = await messages .firstWhere((msg) => msg['seq'] == seq && msg['opcode'] == 57) .timeout(const Duration(seconds: 15)); if (response['cmd'] == 3) { final errorPayload = response['payload'] ?? {}; final errorMessage = errorPayload['localizedMessage'] ?? errorPayload['message'] ?? 'Неизвестная ошибка'; print('Ошибка присоединения к группе: $errorMessage'); throw Exception(errorMessage); } if (response['cmd'] == 1 && response['payload'] != null) { print('Успешно присоединились: ${response['payload']}'); return response['payload'] as Map; } else { print('Неожиданный ответ на joinGroupByLink: $response'); throw Exception('Неверный ответ от сервера'); } } on TimeoutException { print('Таймаут ожидания ответа на joinGroupByLink (seq: $seq)'); throw Exception('Сервер не ответил вовремя'); } catch (e) { print('Ошибка в joinGroupByLink: $e'); rethrow; } } Future searchContactByPhone(String phone) async { await waitUntilOnline(); final payload = {'phone': phone}; _sendMessage(46, payload); print('Запрос на поиск контакта отправлен с payload: $payload'); } Future searchChannels(String query) async { await waitUntilOnline(); final payload = {'contactIds': []}; _sendMessage(32, payload); print('Запрос на поиск каналов отправлен с payload: $payload'); } Future enterChannel(String link) async { await waitUntilOnline(); final payload = {'link': link}; _sendMessage(89, payload); print('Запрос на вход в канал отправлен с payload: $payload'); } Future subscribeToChannel(String link) async { await waitUntilOnline(); final payload = {'link': link}; _sendMessage(57, payload); print('Запрос на подписку на канал отправлен с payload: $payload'); } Future> getChatsAndContacts({bool force = false}) async { await waitUntilOnline(); if (authToken == null) { print("Токен авторизации не найден, требуется повторная авторизация"); throw Exception("Auth token not found - please re-authenticate"); } if (!force && _lastChatsPayload != null && _lastChatsAt != null) { if (DateTime.now().difference(_lastChatsAt!) < _chatsCacheTtl) { return _lastChatsPayload!; } } if (_chatsFetchedInThisSession && _lastChatsPayload != null && !force) { return _lastChatsPayload!; } if (_inflightChatsCompleter != null) { return _inflightChatsCompleter!.future; } _inflightChatsCompleter = Completer>(); if (_isSessionOnline && _isSessionReady && _lastChatsPayload != null && !force) { _inflightChatsCompleter!.complete(_lastChatsPayload!); _inflightChatsCompleter = null; return _lastChatsPayload!; } try { Map chatResponse; final int opcode; final Map payload; final prefs = await SharedPreferences.getInstance(); final deviceId = prefs.getString('spoof_deviceid') ?? generateRandomDeviceId(); if (prefs.getString('spoof_deviceid') == null) { await prefs.setString('spoof_deviceid', deviceId); } if (!_chatsFetchedInThisSession) { opcode = 19; payload = { "chatsCount": 100, "chatsSync": 0, "contactsSync": 0, "draftsSync": 0, "interactive": true, "presenceSync": 0, "token": authToken, }; if (userId != null) { payload["userId"] = userId; } } else { return await getChatsOnly(force: force); } final int chatSeq = _sendMessage(opcode, payload); chatResponse = await messages.firstWhere((msg) => msg['seq'] == chatSeq); if (opcode == 19 && chatResponse['cmd'] == 1) { print("✅ Авторизация (opcode 19) успешна. Сессия ГОТОВА."); _isSessionReady = true; // <-- ВОТ ТЕПЕРЬ СЕССИЯ ПОЛНОСТЬЮ ГОТОВА! _connectionStatusController.add("ready"); final profile = chatResponse['payload']?['profile']; final contactProfile = profile?['contact']; if (contactProfile != null && contactProfile['id'] != null) { print( "[getChatsAndContacts] ✅ Профиль и ID пользователя найдены. ID: ${contactProfile['id']}. ЗАПУСКАЕМ АНАЛИТИКУ.", ); _userId = contactProfile['id']; _sessionId = DateTime.now().millisecondsSinceEpoch; _lastActionTime = _sessionId; sendNavEvent('COLD_START'); _sendInitialSetupRequests(); } else { print( "[getChatsAndContacts] ❌ ВНИМАНИЕ: Профиль или ID в ответе пустой, аналитика не будет отправлена.", ); } if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) { _onlineCompleter!.complete(); } _startPinging(); _processMessageQueue(); } final profile = chatResponse['payload']?['profile']; final presence = chatResponse['payload']?['presence']; final config = chatResponse['payload']?['config']; final List chatListJson = chatResponse['payload']?['chats'] ?? []; if (chatListJson.isEmpty) { if (config != null) { _processServerPrivacyConfig(config); } final result = { 'chats': [], 'contacts': [], 'profile': profile, 'config': config, }; _lastChatsPayload = result; _lastChatsAt = DateTime.now(); _chatsFetchedInThisSession = true; _inflightChatsCompleter!.complete(_lastChatsPayload!); _inflightChatsCompleter = null; return result; } final contactIds = {}; for (var chatJson in chatListJson) { final participants = chatJson['participants'] as Map; contactIds.addAll(participants.keys.map((id) => int.parse(id))); } final int contactSeq = _sendMessage(32, { "contactIds": contactIds.toList(), }); final contactResponse = await messages.firstWhere( (msg) => msg['seq'] == contactSeq, ); final List contactListJson = contactResponse['payload']?['contacts'] ?? []; if (presence != null) { updatePresenceData(presence); } if (config != null) { _processServerPrivacyConfig(config); } final result = { 'chats': chatListJson, 'contacts': contactListJson, 'profile': profile, 'presence': presence, 'config': config, }; _lastChatsPayload = result; final contacts = contactListJson .map((json) => Contact.fromJson(json)) .toList(); updateContactCache(contacts); _lastChatsAt = DateTime.now(); _chatsFetchedInThisSession = true; _inflightChatsCompleter!.complete(result); _inflightChatsCompleter = null; return result; } catch (e) { final error = e; _inflightChatsCompleter?.completeError(error); _inflightChatsCompleter = null; rethrow; } } Future> getMessageHistory( int chatId, { bool force = false, }) async { if (!force && _messageCache.containsKey(chatId)) { print("Загружаем сообщения для чата $chatId из кэша."); return _messageCache[chatId]!; } print("Запрашиваем историю для чата $chatId с сервера."); final payload = { "chatId": chatId, "from": DateTime.now() .add(const Duration(days: 1)) .millisecondsSinceEpoch, "forward": 0, "backward": 1000, // Увеличиваем лимит для получения всех сообщений "getMessages": true, }; try { final int seq = _sendMessage(49, payload); final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 15)); if (response['cmd'] == 3) { final error = response['payload']; print('Ошибка получения истории сообщений: $error'); if (error['error'] == 'proto.state') { print( 'Ошибка состояния сессии при получении истории, переподключаемся...', ); await reconnect(); await waitUntilOnline(); return getMessageHistory(chatId, force: true); } throw Exception('Ошибка получения истории: ${error['message']}'); } final List messagesJson = response['payload']?['messages'] ?? []; final messagesList = messagesJson.map((json) => Message.fromJson(json)).toList() ..sort((a, b) => a.time.compareTo(b.time)); _messageCache[chatId] = messagesList; return messagesList; } catch (e) { print('Ошибка при получении истории сообщений: $e'); return []; } } Future?> loadOldMessages( int chatId, String fromMessageId, int count, ) async { print( "Запрашиваем старые сообщения для чата $chatId начиная с $fromMessageId", ); final payload = { "chatId": chatId, "from": int.parse(fromMessageId), "forward": 0, "backward": count, "getMessages": true, }; try { final int seq = _sendMessage(49, payload); final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 15)); if (response['cmd'] == 3) { final error = response['payload']; print('Ошибка получения старых сообщений: $error'); return null; } return response['payload']; } catch (e) { print('Ошибка при получении старых сообщений: $e'); return null; } } void setAppInForeground(bool isForeground) { _isAppInForeground = isForeground; } void sendNavEvent(String event, {int? screenTo, int? screenFrom}) { if (_userId == null) return; final now = DateTime.now().millisecondsSinceEpoch; final Map params = { 'session_id': _sessionId, 'action_id': _actionId++, }; switch (event) { case 'COLD_START': if (_isColdStartSent) return; params['screen_to'] = 150; params['source_id'] = 1; _isColdStartSent = true; break; case 'WARM_START': params['screen_to'] = 150; // Предполагаем, что всегда возвращаемся на главный экран params['screen_from'] = 1; // 1 = приложение свернуто params['prev_time'] = _lastActionTime; break; case 'GO': params['screen_to'] = screenTo; params['screen_from'] = screenFrom; params['prev_time'] = _lastActionTime; break; } _lastActionTime = now; _sendMessage(5, { "events": [ { "type": "NAV", "event": event, "userId": _userId, "time": now, "params": params, }, ], }); } void createFolder( String title, { List? include, List? filters, }) { final folderId = const Uuid().v4(); final payload = { "id": folderId, "title": title, "include": include ?? [], "filters": filters ?? [], }; _sendMessage(274, payload); print('Создаем папку: $title (ID: $folderId)'); } void updateFolder( String folderId, { String? title, List? include, List? filters, }) { final payload = { "id": folderId, if (title != null) "title": title, if (include != null) "include": include, if (filters != null) "filters": filters, }; _sendMessage(274, payload); print('Обновляем папку: $folderId'); } Future _sendInitialSetupRequests() async { print("Запускаем отправку единичных запросов при старте..."); await Future.delayed(const Duration(seconds: 2)); _sendMessage(272, {"folderSync": 0}); await Future.delayed(const Duration(milliseconds: 500)); _sendMessage(27, {"sync": 0, "type": "STICKER"}); await Future.delayed(const Duration(milliseconds: 500)); _sendMessage(27, {"sync": 0, "type": "FAVORITE_STICKER"}); await Future.delayed(const Duration(milliseconds: 500)); _sendMessage(79, {"forward": false, "count": 100}); await Future.delayed(const Duration(seconds: 5)); _sendMessage(26, { "sectionId": "NEW_STICKER_SETS", "from": 5, "count": 100, }); print("Единичные запросы отправлены."); } void clearCacheForChat(int chatId) { _messageCache.remove(chatId); print("Кэш для чата $chatId очищен."); } void clearChatsCache() { _lastChatsPayload = null; _lastChatsAt = null; print("Кэш чатов очищен."); } Contact? getCachedContact(int contactId) { if (_contactCache.containsKey(contactId)) { final contact = _contactCache[contactId]!; print('Контакт $contactId получен из кэша: ${contact.name}'); return contact; } return null; } Future> getNetworkStatistics() async { final prefs = await SharedPreferences.getInstance(); final totalTraffic = prefs.getDouble('network_total_traffic') ?? (150.0 * 1024 * 1024); // 150 MB по умолчанию final messagesTraffic = prefs.getDouble('network_messages_traffic') ?? (totalTraffic * 0.15); final mediaTraffic = prefs.getDouble('network_media_traffic') ?? (totalTraffic * 0.6); final syncTraffic = prefs.getDouble('network_sync_traffic') ?? (totalTraffic * 0.1); final currentSpeed = _isSessionOnline ? 512.0 * 1024 : 0.0; // 512 KB/s если онлайн final ping = 25; return { 'totalTraffic': totalTraffic, 'messagesTraffic': messagesTraffic, 'mediaTraffic': mediaTraffic, 'syncTraffic': syncTraffic, 'otherTraffic': totalTraffic * 0.15, 'currentSpeed': currentSpeed, 'isConnected': _isSessionOnline, 'connectionType': 'Wi-Fi', // Можно определить реальный тип 'signalStrength': 85, 'ping': ping, 'jitter': 2.5, 'packetLoss': 0.01, 'hourlyStats': [], // Пока пустой массив, можно реализовать позже }; } bool isContactCacheValid() { if (_lastContactsUpdate == null) return false; return DateTime.now().difference(_lastContactsUpdate!) < _contactCacheExpiry; } void updateContactCache(List contacts) { _contactCache.clear(); for (final contact in contacts) { _contactCache[contact.id] = contact; } _lastContactsUpdate = DateTime.now(); print('Кэш контактов обновлен: ${contacts.length} контактов'); } void updateCachedContact(Contact contact) { _contactCache[contact.id] = contact; print('Контакт ${contact.id} обновлен в кэше: ${contact.name}'); } void clearContactCache() { _contactCache.clear(); _lastContactsUpdate = null; print("Кэш контактов очищен."); } void clearAllCaches() { clearContactCache(); clearChatsCache(); _messageCache.clear(); clearPasswordAuthData(); print("Все кэши очищены из-за ошибки подключения."); } Future clearAllData() async { try { clearAllCaches(); authToken = null; final prefs = await SharedPreferences.getInstance(); await prefs.clear(); _pingTimer?.cancel(); await _channel?.sink.close(); _channel = null; _isSessionOnline = false; _isSessionReady = false; _chatsFetchedInThisSession = false; _reconnectAttempts = 0; _currentUrlIndex = 0; _messageQueue.clear(); _presenceData.clear(); print("Все данные приложения полностью очищены."); } catch (e) { print("Ошибка при полной очистке данных: $e"); rethrow; } } void sendMessage( int chatId, String text, { String? replyToMessageId, int? cid, }) { final int clientMessageId = cid ?? DateTime.now().millisecondsSinceEpoch; final payload = { "chatId": chatId, "message": { "text": text, "cid": clientMessageId, "elements": [], "attaches": [], if (replyToMessageId != null) "link": {"type": "REPLY", "messageId": replyToMessageId}, }, "notify": true, }; clearChatsCache(); if (_isSessionOnline) { _sendMessage(64, payload); } else { print("Сессия не онлайн. Сообщение добавлено в очередь."); _messageQueue.add({'opcode': 64, 'payload': payload}); } } void forwardMessage(int targetChatId, String messageId, int sourceChatId) { final int clientMessageId = DateTime.now().millisecondsSinceEpoch; final payload = { "chatId": targetChatId, "message": { "cid": clientMessageId, "link": { "type": "FORWARD", "messageId": messageId, "chatId": sourceChatId, }, "attaches": [], }, "notify": true, }; if (_isSessionOnline) { _sendMessage(64, payload); } else { _messageQueue.add({'opcode': 64, 'payload': payload}); } } void _processMessageQueue() { if (_messageQueue.isEmpty) return; print("Отправка ${_messageQueue.length} сообщений из очереди..."); for (var message in _messageQueue) { _sendMessage(message['opcode'], message['payload']); } _messageQueue.clear(); } Future editMessage(int chatId, String messageId, String newText) async { final payload = { "chatId": chatId, "messageId": messageId, "text": newText, "elements": [], "attachments": [], }; clearChatsCache(); await waitUntilOnline(); if (!_isSessionOnline) { print('Сессия не онлайн, пытаемся переподключиться...'); await reconnect(); await waitUntilOnline(); } Future sendOnce() async { try { final int seq = _sendMessage(67, payload); final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 10)); if (response['cmd'] == 3) { final error = response['payload']; print('Ошибка редактирования сообщения: $error'); if (error['error'] == 'proto.state') { print('Ошибка состояния сессии, переподключаемся...'); await reconnect(); await waitUntilOnline(); return false; // Попробуем еще раз } if (error['error'] == 'error.edit.invalid.message') { print( 'Сообщение не может быть отредактировано: ${error['localizedMessage']}', ); throw Exception( 'Сообщение не может быть отредактировано: ${error['localizedMessage']}', ); } return false; } return response['cmd'] == 1; // Успешный ответ } catch (e) { print('Ошибка при редактировании сообщения: $e'); return false; } } for (int attempt = 0; attempt < 3; attempt++) { print( 'Попытка редактирования сообщения $messageId (попытка ${attempt + 1}/3)', ); bool ok = await sendOnce(); if (ok) { print('Сообщение $messageId успешно отредактировано'); return; } if (attempt < 2) { print( 'Повторяем запрос редактирования для сообщения $messageId через 2 секунды...', ); await Future.delayed(const Duration(seconds: 2)); } } print('Не удалось отредактировать сообщение $messageId после 3 попыток'); } Future deleteMessage( int chatId, String messageId, { bool forMe = false, }) async { final payload = { "chatId": chatId, "messageIds": [messageId], "forMe": forMe, }; clearChatsCache(); await waitUntilOnline(); if (!_isSessionOnline) { print('Сессия не онлайн, пытаемся переподключиться...'); await reconnect(); await waitUntilOnline(); } Future sendOnce() async { try { final int seq = _sendMessage(66, payload); final response = await messages .firstWhere((msg) => msg['seq'] == seq) .timeout(const Duration(seconds: 10)); if (response['cmd'] == 3) { final error = response['payload']; print('Ошибка удаления сообщения: $error'); if (error['error'] == 'proto.state') { print('Ошибка состояния сессии, переподключаемся...'); await reconnect(); await waitUntilOnline(); return false; // Попробуем еще раз } return false; } return response['cmd'] == 1; // Успешный ответ } catch (e) { print('Ошибка при удалении сообщения: $e'); return false; } } for (int attempt = 0; attempt < 3; attempt++) { print('Попытка удаления сообщения $messageId (попытка ${attempt + 1}/3)'); bool ok = await sendOnce(); if (ok) { print('Сообщение $messageId успешно удалено'); return; } if (attempt < 2) { print( 'Повторяем запрос удаления для сообщения $messageId через 2 секунды...', ); await Future.delayed(const Duration(seconds: 2)); } } print('Не удалось удалить сообщение $messageId после 3 попыток'); } void sendTyping(int chatId, {String type = "TEXT"}) { final payload = {"chatId": chatId, "type": type}; if (_isSessionOnline) { _sendMessage(65, payload); } } void updateProfileText( String firstName, String lastName, String description, ) { final payload = { "firstName": firstName, "lastName": lastName, "description": description, }; _sendMessage(16, payload); } Future updateProfilePhoto(String firstName, String lastName) async { try { final picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image == null) return; print("Запрашиваем URL для загрузки фото..."); final int seq = _sendMessage(80, {"count": 1}); final response = await messages.firstWhere((msg) => msg['seq'] == seq); final String uploadUrl = response['payload']['url']; print("URL получен: $uploadUrl"); print("Загружаем фото на сервер..."); var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); request.files.add(await http.MultipartFile.fromPath('file', image.path)); var streamedResponse = await request.send(); var httpResponse = await http.Response.fromStream(streamedResponse); if (httpResponse.statusCode != 200) { throw Exception("Ошибка загрузки фото: ${httpResponse.body}"); } final uploadResult = jsonDecode(httpResponse.body); final String photoToken = uploadResult['photos'].values.first['token']; print("Фото загружено, получен токен: $photoToken"); print("Привязываем фото к профилю..."); final payload = { "firstName": firstName, "lastName": lastName, "photoToken": photoToken, "avatarType": "USER_AVATAR", }; _sendMessage(16, payload); print("Запрос на смену аватара отправлен."); } catch (e) { print("!!! Ошибка в процессе смены аватара: $e"); } } Future sendPhotoMessage( int chatId, { String? localPath, String? caption, int? cidOverride, int? senderId, // my user id to mark local echo as mine }) async { try { XFile? image; if (localPath != null) { image = XFile(localPath); } else { final picker = ImagePicker(); image = await picker.pickImage(source: ImageSource.gallery); if (image == null) return; } await waitUntilOnline(); final int seq80 = _sendMessage(80, {"count": 1}); final resp80 = await messages.firstWhere((m) => m['seq'] == seq80); final String uploadUrl = resp80['payload']['url']; var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); request.files.add(await http.MultipartFile.fromPath('file', image.path)); var streamed = await request.send(); var httpResp = await http.Response.fromStream(streamed); if (httpResp.statusCode != 200) { throw Exception( 'Ошибка загрузки фото: ${httpResp.statusCode} ${httpResp.body}', ); } final uploadJson = jsonDecode(httpResp.body) as Map; final Map photos = uploadJson['photos'] as Map; if (photos.isEmpty) throw Exception('Не получен токен фото'); final String photoToken = (photos.values.first as Map)['token']; final int cid = cidOverride ?? DateTime.now().millisecondsSinceEpoch; final payload = { "chatId": chatId, "message": { "text": caption?.trim() ?? "", "cid": cid, "elements": [], "attaches": [ {"_type": "PHOTO", "photoToken": photoToken}, ], }, "notify": true, }; clearChatsCache(); if (localPath != null) { _emitLocal({ 'ver': 11, 'cmd': 1, 'seq': -1, 'opcode': 128, 'payload': { 'chatId': chatId, 'message': { 'id': 'local_$cid', 'sender': senderId ?? 0, 'time': DateTime.now().millisecondsSinceEpoch, 'text': caption?.trim() ?? '', 'type': 'USER', 'cid': cid, 'attaches': [ {'_type': 'PHOTO', 'url': 'file://$localPath'}, ], }, }, }); } _sendMessage(64, payload); } catch (e) { print('Ошибка отправки фото-сообщения: $e'); } } Future sendPhotoMessages( int chatId, { required List localPaths, String? caption, int? senderId, }) async { if (localPaths.isEmpty) return; try { await waitUntilOnline(); final int cid = DateTime.now().millisecondsSinceEpoch; _emitLocal({ 'ver': 11, 'cmd': 1, 'seq': -1, 'opcode': 128, 'payload': { 'chatId': chatId, 'message': { 'id': 'local_$cid', 'sender': senderId ?? 0, 'time': DateTime.now().millisecondsSinceEpoch, 'text': caption?.trim() ?? '', 'type': 'USER', 'cid': cid, 'attaches': [ for (final p in localPaths) {'_type': 'PHOTO', 'url': 'file://$p'}, ], }, }, }); final List> photoTokens = []; for (final path in localPaths) { final int seq80 = _sendMessage(80, {"count": 1}); final resp80 = await messages.firstWhere((m) => m['seq'] == seq80); final String uploadUrl = resp80['payload']['url']; var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); request.files.add(await http.MultipartFile.fromPath('file', path)); var streamed = await request.send(); var httpResp = await http.Response.fromStream(streamed); if (httpResp.statusCode != 200) { throw Exception( 'Ошибка загрузки фото: ${httpResp.statusCode} ${httpResp.body}', ); } final uploadJson = jsonDecode(httpResp.body) as Map; final Map photos = uploadJson['photos'] as Map; if (photos.isEmpty) throw Exception('Не получен токен фото'); final String photoToken = (photos.values.first as Map)['token']; photoTokens.add({"token": photoToken}); } final payload = { "chatId": chatId, "message": { "text": caption?.trim() ?? "", "cid": cid, "elements": [], "attaches": [ for (final t in photoTokens) {"_type": "PHOTO", "photoToken": t["token"]}, ], }, "notify": true, }; clearChatsCache(); _sendMessage(64, payload); } catch (e) { print('Ошибка отправки фото-сообщений: $e'); } } Future sendFileMessage( int chatId, { String? caption, int? senderId, // my user id to mark local echo as mine }) async { try { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.any, ); if (result == null || result.files.single.path == null) { print("Выбор файла отменен"); return; } final String filePath = result.files.single.path!; final String fileName = result.files.single.name; final int fileSize = result.files.single.size; await waitUntilOnline(); final int seq87 = _sendMessage(87, {"count": 1}); final resp87 = await messages.firstWhere((m) => m['seq'] == seq87); if (resp87['payload'] == null || resp87['payload']['info'] == null || (resp87['payload']['info'] as List).isEmpty) { throw Exception('Неверный ответ на Opcode 87: отсутствует "info"'); } final uploadInfo = (resp87['payload']['info'] as List).first; final String uploadUrl = uploadInfo['url']; final int fileId = uploadInfo['fileId']; // <-- Ключевое отличие от фото print('Получен fileId: $fileId и URL: $uploadUrl'); var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); request.files.add(await http.MultipartFile.fromPath('file', filePath)); var streamed = await request.send(); var httpResp = await http.Response.fromStream(streamed); if (httpResp.statusCode != 200) { throw Exception( 'Ошибка загрузки файла: ${httpResp.statusCode} ${httpResp.body}', ); } print('Файл успешно загружен на сервер.'); final int cid = DateTime.now().millisecondsSinceEpoch; final payload = { "chatId": chatId, "message": { "text": caption?.trim() ?? "", "cid": cid, "elements": [], "attaches": [ {"_type": "FILE", "fileId": fileId}, // <-- Используем fileId ], }, "notify": true, }; clearChatsCache(); _emitLocal({ 'ver': 11, 'cmd': 1, 'seq': -1, 'opcode': 128, 'payload': { 'chatId': chatId, 'message': { 'id': 'local_$cid', 'sender': senderId ?? 0, 'time': DateTime.now().millisecondsSinceEpoch, 'text': caption?.trim() ?? '', 'type': 'USER', 'cid': cid, 'attaches': [ { '_type': 'FILE', 'name': fileName, 'size': fileSize, 'url': 'file://$filePath', // Локальный путь для UI }, ], }, }, }); _sendMessage(64, payload); print('Сообщение о файле (Opcode 64) отправлено.'); } catch (e) { print('Ошибка отправки файла: $e'); } } void _startPinging() { _pingTimer?.cancel(); _pingTimer = Timer.periodic(const Duration(seconds: 25), (timer) { if (_isSessionOnline && _isSessionReady) { print("Отправляем Ping для поддержания сессии..."); _sendMessage(1, {"interactive": true}); } else { print("Сессия не готова, пропускаем ping"); } }); } Future saveToken(String token, {String? userId}) async { print("Сохраняем новый токен: ${token.substring(0, 20)}..."); if (userId != null) { print("Сохраняем UserID: $userId"); } authToken = token; final prefs = await SharedPreferences.getInstance(); await prefs.setString('authToken', token); disconnect(); await connect(); await getChatsAndContacts(force: true); if (userId != null) { await prefs.setString('userId', userId); } print("Токен и UserID успешно сохранены в SharedPreferences"); } Future hasToken() async { if (authToken == null) { final prefs = await SharedPreferences.getInstance(); authToken = prefs.getString('authToken'); userId = prefs.getString('userId'); if (authToken != null) { print( "Токен загружен из SharedPreferences: ${authToken!.substring(0, 20)}...", ); if (userId != null) { print("UserID загружен из SharedPreferences: $userId"); } } } return authToken != null; } Future> fetchContactsByIds(List contactIds) async { if (contactIds.isEmpty) { return []; } print('Запрашиваем данные для ${contactIds.length} контактов...'); try { final int contactSeq = _sendMessage(32, {"contactIds": contactIds}); final contactResponse = await messages .firstWhere((msg) => msg['seq'] == contactSeq) .timeout(const Duration(seconds: 10)); if (contactResponse['cmd'] == 3) { print( "Ошибка при получении контактов по ID: ${contactResponse['payload']}", ); return []; } final List contactListJson = contactResponse['payload']?['contacts'] ?? []; final contacts = contactListJson .map((json) => Contact.fromJson(json)) .toList(); for (final contact in contacts) { _contactCache[contact.id] = contact; } print("Получены и закэшированы данные для ${contacts.length} контактов."); return contacts; } catch (e) { print('Исключение при получении контактов по ID: $e'); return []; } } Future logout() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('authToken'); await prefs.remove('userId'); authToken = null; userId = null; _messageCache.clear(); _lastChatsPayload = null; _chatsFetchedInThisSession = false; _pingTimer?.cancel(); await _channel?.sink.close(status.goingAway); _channel = null; } catch (_) {} } Future connect() async { if (_channel != null && _isSessionOnline) { print("WebSocket уже подключен, пропускаем подключение"); return; } print("Запускаем подключение к WebSocket..."); _isSessionOnline = false; _isSessionReady = false; _connectionStatusController.add("connecting"); await _connectWithFallback(); } Future reconnect() async { _reconnectAttempts = 0; // Сбрасываем счетчик попыток при ручном переподключении _currentUrlIndex = 0; // Сбрасываем индекс для повторной попытки _connectionStatusController.add("connecting"); await _connectWithFallback(); } void sendFullJsonRequest(String jsonString) { if (_channel == null) { throw Exception('WebSocket is not connected. Connect first.'); } _log('➡️ SEND (raw): $jsonString'); _channel!.sink.add(jsonString); } int sendRawRequest(int opcode, Map payload) { if (_channel == null) { print('WebSocket не подключен!'); throw Exception('WebSocket is not connected. Connect first.'); } return _sendMessage(opcode, payload); } int sendAndTrackFullJsonRequest(String jsonString) { if (_channel == null) { throw Exception('WebSocket is not connected. Connect first.'); } final message = jsonDecode(jsonString) as Map; final int currentSeq = _seq++; message['seq'] = currentSeq; final encodedMessage = jsonEncode(message); _log('➡️ SEND (custom): $encodedMessage'); print('Отправляем кастомное сообщение (seq: $currentSeq): $encodedMessage'); _channel!.sink.add(encodedMessage); return currentSeq; } int _sendMessage(int opcode, Map payload) { if (_channel == null) { print('WebSocket не подключен!'); return -1; } final message = { "ver": 11, "cmd": 0, "seq": _seq, "opcode": opcode, "payload": payload, }; final encodedMessage = jsonEncode(message); if (opcode == 1) { _log('➡️ SEND (ping) seq: $_seq'); } else if (opcode == 18 || opcode == 19) { Map loggablePayload = Map.from(payload); if (loggablePayload.containsKey('token')) { String token = loggablePayload['token'] as String; loggablePayload['token'] = token.length > 8 ? '${token.substring(0, 4)}...${token.substring(token.length - 4)}' : '***'; } final loggableMessage = {...message, 'payload': loggablePayload}; _log('➡️ SEND: ${jsonEncode(loggableMessage)}'); } else { _log('➡️ SEND: $encodedMessage'); } print('Отправляем сообщение (seq: $_seq): $encodedMessage'); _channel!.sink.add(encodedMessage); return _seq++; } void _listen() async { _streamSubscription?.cancel(); // Отменяем предыдущую подписку _streamSubscription = _channel?.stream.listen( (message) { if (message == null) return; if (message is String && message.trim().isEmpty) { return; } String loggableMessage = message; try { final decoded = jsonDecode(message) as Map; if (decoded['opcode'] == 2) { loggableMessage = '⬅️ RECV (pong) seq: ${decoded['seq']}'; } else { Map loggableDecoded = Map.from(decoded); bool wasModified = false; if (loggableDecoded.containsKey('payload') && loggableDecoded['payload'] is Map) { Map payload = Map.from( loggableDecoded['payload'], ); if (payload.containsKey('token')) { String token = payload['token'] as String; payload['token'] = token.length > 8 ? '${token.substring(0, 4)}...${token.substring(token.length - 4)}' : '***'; loggableDecoded['payload'] = payload; wasModified = true; } } if (wasModified) { loggableMessage = '⬅️ RECV: ${jsonEncode(loggableDecoded)}'; } else { loggableMessage = '⬅️ RECV: $message'; } } } catch (_) { loggableMessage = '⬅️ RECV (raw): $message'; } _log(loggableMessage); try { final decodedMessage = message is String ? jsonDecode(message) : message; if (decodedMessage is Map && decodedMessage['opcode'] == 97 && decodedMessage['cmd'] == 1 && decodedMessage['payload'] != null && decodedMessage['payload']['token'] != null) { _handleSessionTerminated(); return; } if (decodedMessage is Map && decodedMessage['opcode'] == 6 && decodedMessage['cmd'] == 1) { print("Handshake успешен. Сессия ONLINE."); _isSessionOnline = true; _isSessionReady = false; _reconnectDelaySeconds = 2; _connectionStatusController.add("authorizing"); if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) { _onlineCompleter!.complete(); } _startPinging(); _processMessageQueue(); } if (decodedMessage is Map && decodedMessage['cmd'] == 3) { final error = decodedMessage['payload']; print('Ошибка сервера: $error'); if (error != null && error['localizedMessage'] != null) { _errorController.add(error['localizedMessage']); } else if (error != null && error['message'] != null) { _errorController.add(error['message']); } if (error != null && error['message'] == 'FAIL_WRONG_PASSWORD') { _errorController.add('FAIL_WRONG_PASSWORD'); } if (error != null && error['error'] == 'password.invalid') { _errorController.add('Неверный пароль'); } if (error != null && error['error'] == 'proto.state') { print('Ошибка состояния сессии, переподключаемся...'); _chatsFetchedInThisSession = false; _reconnect(); return; } if (error != null && error['error'] == 'login.token') { print('Токен недействителен, очищаем и завершаем сессию...'); _handleInvalidToken(); return; } if (error != null && error['message'] == 'FAIL_WRONG_PASSWORD') { print('Неверный токен авторизации, очищаем токен...'); _clearAuthToken().then((_) { _chatsFetchedInThisSession = false; _messageController.add({ 'type': 'invalid_token', 'message': 'Токен авторизации недействителен. Требуется повторная авторизация.', }); _reconnect(); }); return; } } if (decodedMessage is Map && decodedMessage['opcode'] == 18 && decodedMessage['cmd'] == 1 && decodedMessage['payload'] != null) { final payload = decodedMessage['payload']; if (payload['passwordChallenge'] != null) { final challenge = payload['passwordChallenge']; _currentPasswordTrackId = challenge['trackId']; _currentPasswordHint = challenge['hint']; _currentPasswordEmail = challenge['email']; print( 'Получен запрос на ввод пароля: trackId=${challenge['trackId']}, hint=${challenge['hint']}, email=${challenge['email']}', ); _messageController.add({ 'type': 'password_required', 'trackId': _currentPasswordTrackId, 'hint': _currentPasswordHint, 'email': _currentPasswordEmail, }); return; } } if (decodedMessage is Map && decodedMessage['opcode'] == 22 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Настройки приватности успешно обновлены: $payload'); _messageController.add({ 'type': 'privacy_settings_updated', 'settings': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 116 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Пароль успешно установлен: $payload'); _messageController.add({ 'type': 'password_set_success', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 57 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Успешно присоединились к группе: $payload'); _messageController.add({ 'type': 'group_join_success', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 46 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Контакт найден: $payload'); _messageController.add({ 'type': 'contact_found', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 46 && decodedMessage['cmd'] == 3) { final payload = decodedMessage['payload']; print('Контакт не найден: $payload'); _messageController.add({ 'type': 'contact_not_found', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 32 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Каналы найдены: $payload'); _messageController.add({ 'type': 'channels_found', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 32 && decodedMessage['cmd'] == 3) { final payload = decodedMessage['payload']; print('Каналы не найдены: $payload'); _messageController.add({ 'type': 'channels_not_found', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 89 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Вход в канал успешен: $payload'); _messageController.add({ 'type': 'channel_entered', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 89 && decodedMessage['cmd'] == 3) { final payload = decodedMessage['payload']; print('Ошибка входа в канал: $payload'); _messageController.add({ 'type': 'channel_error', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 57 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Подписка на канал успешна: $payload'); _messageController.add({ 'type': 'channel_subscribed', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 57 && decodedMessage['cmd'] == 3) { final payload = decodedMessage['payload']; print('Ошибка подписки на канал: $payload'); _messageController.add({ 'type': 'channel_error', 'payload': payload, }); } if (decodedMessage is Map && decodedMessage['opcode'] == 59 && decodedMessage['cmd'] == 1) { final payload = decodedMessage['payload']; print('Получены участники группы: $payload'); _messageController.add({ 'type': 'group_members', 'payload': payload, }); } if (decodedMessage is Map) { _messageController.add(decodedMessage); } } catch (e) { print('Невалидное сообщение от сервера, пропускаем: $e'); } }, onError: (error) { print('Ошибка WebSocket: $error'); _isSessionOnline = false; _isSessionReady = false; _reconnect(); }, onDone: () { print('WebSocket соединение закрыто. Попытка переподключения...'); _isSessionOnline = false; _isSessionReady = false; if (!_isSessionReady) { _reconnect(); } }, cancelOnError: true, ); } void _reconnect() { if (_isReconnecting) return; _isReconnecting = true; _reconnectAttempts++; if (_reconnectAttempts > _maxReconnectAttempts) { print( "Превышено максимальное количество попыток переподключения ($_maxReconnectAttempts). Останавливаем попытки.", ); _connectionStatusController.add("disconnected"); _isReconnecting = false; return; } _pingTimer?.cancel(); _reconnectTimer?.cancel(); _isSessionOnline = false; _isSessionReady = false; _onlineCompleter = Completer(); _chatsFetchedInThisSession = false; clearAllCaches(); _currentUrlIndex = 0; _reconnectDelaySeconds = (_reconnectDelaySeconds * 2).clamp(1, 30); final jitter = (DateTime.now().millisecondsSinceEpoch % 1000) / 1000.0; final delay = Duration(seconds: _reconnectDelaySeconds + jitter.round()); _reconnectTimer = Timer(delay, () { print( "Переподключаемся после ${delay.inSeconds}s... (попытка $_reconnectAttempts/$_maxReconnectAttempts)", ); _isReconnecting = false; _connectWithFallback(); }); } Future getVideoUrl(int videoId, int chatId, String messageId) async { await waitUntilOnline(); final payload = { "videoId": videoId, "chatId": chatId, "messageId": messageId, }; final int seq = _sendMessage(83, payload); print('Запрашиваем URL для videoId: $videoId (seq: $seq)'); try { final response = await messages .firstWhere((msg) => msg['seq'] == seq && msg['opcode'] == 83) .timeout(const Duration(seconds: 15)); if (response['cmd'] == 3) { throw Exception( 'Ошибка получения URL видео: ${response['payload']?['message']}', ); } final videoPayload = response['payload'] as Map?; if (videoPayload == null) { throw Exception('Получен пустой payload для видео'); } String? videoUrl = videoPayload['MP4_720'] as String? ?? videoPayload['MP4_480'] as String? ?? videoPayload['MP4_1080'] as String? ?? videoPayload['MP4_360'] as String?; if (videoUrl == null) { final mp4Key = videoPayload.keys.firstWhere( (k) => k.startsWith('MP4_'), orElse: () => '', ); if (mp4Key.isNotEmpty) { videoUrl = videoPayload[mp4Key] as String?; } } if (videoUrl != null) { print('URL для videoId: $videoId успешно получен.'); return videoUrl; } else { throw Exception('Не найден ни один MP4 URL в ответе'); } } on TimeoutException { print('Таймаут ожидания URL для videoId: $videoId'); throw Exception('Сервер не ответил на запрос видео вовремя'); } catch (e) { print('Ошибка в getVideoUrl: $e'); rethrow; // Передаем ошибку дальше } } void disconnect() { print("Отключаем WebSocket..."); _pingTimer?.cancel(); _reconnectTimer?.cancel(); _streamSubscription?.cancel(); // Отменяем подписку на stream _isSessionOnline = false; _isSessionReady = false; _handshakeSent = false; // Сбрасываем флаг handshake _onlineCompleter = Completer(); _chatsFetchedInThisSession = false; _channel?.sink.close(status.goingAway); _channel = null; _streamSubscription = null; _connectionStatusController.add("disconnected"); } Future getClipboardData() async { final data = await Clipboard.getData(Clipboard.kTextPlain); return data?.text; } void forceReconnect() { print("Принудительное переподключение..."); _pingTimer?.cancel(); _reconnectTimer?.cancel(); if (_channel != null) { print("Закрываем существующее соединение..."); _channel!.sink.close(status.goingAway); _channel = null; } _isReconnecting = false; _reconnectAttempts = 0; _reconnectDelaySeconds = 2; _isSessionOnline = false; _isSessionReady = false; _chatsFetchedInThisSession = false; _currentUrlIndex = 0; _onlineCompleter = Completer(); // Re-create completer clearAllCaches(); _messageQueue.clear(); _presenceData.clear(); _connectionStatusController.add("connecting"); _log("Запускаем новую сессию подключения..."); _connectWithFallback(); } Future performFullReconnection() async { print("🔄 Начинаем полное переподключение..."); try { _pingTimer?.cancel(); _reconnectTimer?.cancel(); _streamSubscription?.cancel(); if (_channel != null) { _channel!.sink.close(); _channel = null; } _isReconnecting = false; _reconnectAttempts = 0; _reconnectDelaySeconds = 2; _isSessionOnline = false; _isSessionReady = false; _handshakeSent = false; _chatsFetchedInThisSession = false; // КРИТИЧНО: сбрасываем этот флаг _currentUrlIndex = 0; _onlineCompleter = Completer(); _seq = 0; _lastChatsPayload = null; _lastChatsAt = null; print( " Кэш чатов очищен: _lastChatsPayload = $_lastChatsPayload, _chatsFetchedInThisSession = $_chatsFetchedInThisSession", ); _connectionStatusController.add("disconnected"); await connect(); print(" Полное переподключение завершено"); await Future.delayed(const Duration(milliseconds: 1500)); if (!_reconnectionCompleteController.isClosed) { print(" Отправляем уведомление о завершении переподключения"); _reconnectionCompleteController.add(null); } } catch (e) { print("Ошибка полного переподключения: $e"); rethrow; } } // ЫЫЫ ХУЯРЮ ВПЕРОД В БОГАЖНЕКЕ ГРАНАТАМЁТ Future updatePrivacySettings({ String? hidden, String? searchByPhone, String? incomingCall, String? chatsInvite, bool? chatsPushNotification, String? chatsPushSound, String? pushSound, bool? mCallPushNotification, bool? pushDetails, bool? contentLevelAccess, }) async { final settings = { if (hidden != null) 'user': {'HIDDEN': hidden == 'true'}, if (searchByPhone != null) 'user': {'SEARCH_BY_PHONE': searchByPhone}, if (incomingCall != null) 'user': {'INCOMING_CALL': incomingCall}, if (chatsInvite != null) 'user': {'CHATS_INVITE': chatsInvite}, if (chatsPushNotification != null) 'user': {'PUSH_NEW_CONTACTS': chatsPushNotification}, if (chatsPushSound != null) 'user': {'PUSH_SOUND': chatsPushSound}, if (pushSound != null) 'user': {'PUSH_SOUND_GLOBAL': pushSound}, if (mCallPushNotification != null) 'user': {'PUSH_MCALL': mCallPushNotification}, if (pushDetails != null) 'user': {'PUSH_DETAILS': pushDetails}, }; print(''); // КАК ЖЕ ПОХУЙ if (hidden != null) { await _updateSinglePrivacySetting({'HIDDEN': hidden == 'true'}); } if (searchByPhone != null) { final seq = searchByPhone == 'ALL' ? 37 : 46; await _updatePrivacySettingWithSeq({ 'SEARCH_BY_PHONE': searchByPhone, }, seq); } if (incomingCall != null) { final seq = incomingCall == 'ALL' ? 30 : 23; await _updatePrivacySettingWithSeq({'INCOMING_CALL': incomingCall}, seq); } if (chatsInvite != null) { final seq = chatsInvite == 'ALL' ? 51 : 55; await _updatePrivacySettingWithSeq({'CHATS_INVITE': chatsInvite}, seq); } if (contentLevelAccess != null) { final seq = contentLevelAccess ? 70 : 62; await _updatePrivacySettingWithSeq({ 'CONTENT_LEVEL_ACCESS': contentLevelAccess, }, seq); } if (chatsPushNotification != null) { await _updateSinglePrivacySetting({ 'PUSH_NEW_CONTACTS': chatsPushNotification, }); } if (chatsPushSound != null) { await _updateSinglePrivacySetting({'PUSH_SOUND': chatsPushSound}); } if (pushSound != null) { await _updateSinglePrivacySetting({'PUSH_SOUND_GLOBAL': pushSound}); } if (mCallPushNotification != null) { await _updateSinglePrivacySetting({'PUSH_MCALL': mCallPushNotification}); } if (pushDetails != null) { await _updateSinglePrivacySetting({'PUSH_DETAILS': pushDetails}); } } Future _updateSinglePrivacySetting(Map setting) async { await waitUntilOnline(); final payload = { 'settings': {'user': setting}, }; _sendMessage(22, payload); print(''); //песюны } Future _updatePrivacySettingWithSeq( Map setting, int seq, ) async { await waitUntilOnline(); final message = { "ver": 11, "cmd": 0, "seq": seq, "opcode": 22, "payload": { "settings": {"user": setting}, }, }; final encodedMessage = jsonEncode(message); _channel?.sink.add(encodedMessage); _log('SEND: $encodedMessage'); print( '', //вроде надо а вроде бля как же похуй ); } void _processServerPrivacyConfig(Map? config) { if (config == null) return; final userConfig = config['user'] as Map?; if (userConfig == null) return; print('Обработка настроек приватности с сервера: $userConfig'); // Сохраняем настройки в SharedPreferences final prefs = SharedPreferences.getInstance(); prefs.then((prefs) { if (userConfig.containsKey('SEARCH_BY_PHONE')) { prefs.setString( 'privacy_search_by_phone', userConfig['SEARCH_BY_PHONE'], ); } if (userConfig.containsKey('INCOMING_CALL')) { prefs.setString('privacy_incoming_call', userConfig['INCOMING_CALL']); } if (userConfig.containsKey('CHATS_INVITE')) { prefs.setString('privacy_chats_invite', userConfig['CHATS_INVITE']); } if (userConfig.containsKey('CONTENT_LEVEL_ACCESS')) { prefs.setBool( 'privacy_content_level_access', userConfig['CONTENT_LEVEL_ACCESS'], ); } if (userConfig.containsKey('HIDDEN')) { prefs.setBool('privacy_hidden', userConfig['HIDDEN']); } }); // Отправляем событие об обновлении настроек _messageController.add({ 'type': 'privacy_settings_updated', 'settings': {'user': userConfig}, }); } void dispose() { _pingTimer?.cancel(); _channel?.sink.close(status.goingAway); _reconnectionCompleteController.close(); _messageController.close(); } }