720 lines
23 KiB
Dart
720 lines
23 KiB
Dart
part of 'api_service.dart';
|
||
|
||
extension ApiServiceConnection on ApiService {
|
||
Future<void> _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<void> _connectToUrl(String url) async {
|
||
_isSessionOnline = false;
|
||
_onlineCompleter = Completer<void>();
|
||
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 = <String, String>{
|
||
'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();
|
||
}
|
||
|
||
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<void> _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 отправлен, ожидаем ответ...');
|
||
}
|
||
|
||
void _startPinging() {
|
||
_pingTimer?.cancel();
|
||
_pingTimer = Timer.periodic(const Duration(seconds: 25), (timer) {
|
||
if (_isSessionOnline && _isSessionReady && _isAppInForeground) {
|
||
print("Отправляем Ping для поддержания сессии...");
|
||
_sendMessage(1, {"interactive": true});
|
||
} else {
|
||
print("Сессия не готова, пропускаем ping");
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> connect() async {
|
||
if (_channel != null && _isSessionOnline) {
|
||
print("WebSocket уже подключен, пропускаем подключение");
|
||
return;
|
||
}
|
||
|
||
print("Запускаем подключение к WebSocket...");
|
||
|
||
_isSessionOnline = false;
|
||
_isSessionReady = false;
|
||
|
||
_connectionStatusController.add("connecting");
|
||
await _connectWithFallback();
|
||
}
|
||
|
||
Future<void> 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<String, dynamic> 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<String, dynamic>;
|
||
|
||
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<String, dynamic> 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<String, dynamic> 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<String, dynamic>;
|
||
if (decoded['opcode'] == 2) {
|
||
loggableMessage = '⬅️ RECV (pong) seq: ${decoded['seq']}';
|
||
} else {
|
||
Map<String, dynamic> loggableDecoded = Map.from(decoded);
|
||
bool wasModified = false;
|
||
if (loggableDecoded.containsKey('payload') &&
|
||
loggableDecoded['payload'] is Map) {
|
||
Map<String, dynamic> 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<String, dynamic>) {
|
||
_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 > ApiService._maxReconnectAttempts) {
|
||
print(
|
||
"Превышено максимальное количество попыток переподключения (${ApiService._maxReconnectAttempts}). Останавливаем попытки.",
|
||
);
|
||
_connectionStatusController.add("disconnected");
|
||
_isReconnecting = false;
|
||
return;
|
||
}
|
||
|
||
_pingTimer?.cancel();
|
||
_reconnectTimer?.cancel();
|
||
_isSessionOnline = false;
|
||
_isSessionReady = false;
|
||
_onlineCompleter = Completer<void>();
|
||
_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/${ApiService._maxReconnectAttempts})",
|
||
);
|
||
_isReconnecting = false;
|
||
_connectWithFallback();
|
||
});
|
||
}
|
||
|
||
void _processMessageQueue() {
|
||
if (_messageQueue.isEmpty) return;
|
||
print("Отправка ${_messageQueue.length} сообщений из очереди...");
|
||
for (var message in _messageQueue) {
|
||
_sendMessage(message['opcode'], message['payload']);
|
||
}
|
||
_messageQueue.clear();
|
||
}
|
||
|
||
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<void>();
|
||
|
||
clearAllCaches();
|
||
_messageQueue.clear();
|
||
_presenceData.clear();
|
||
|
||
_connectionStatusController.add("connecting");
|
||
_log("Запускаем новую сессию подключения...");
|
||
|
||
_connectWithFallback();
|
||
}
|
||
|
||
Future<void> 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<void>();
|
||
_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;
|
||
}
|
||
}
|
||
|
||
void disconnect() {
|
||
print("Отключаем WebSocket...");
|
||
_pingTimer?.cancel();
|
||
_reconnectTimer?.cancel();
|
||
_streamSubscription?.cancel();
|
||
_isSessionOnline = false;
|
||
_isSessionReady = false;
|
||
_handshakeSent = false;
|
||
_onlineCompleter = Completer<void>();
|
||
_chatsFetchedInThisSession = false;
|
||
|
||
_channel?.sink.close(status.goingAway);
|
||
_channel = null;
|
||
_streamSubscription = null;
|
||
|
||
_connectionStatusController.add("disconnected");
|
||
}
|
||
}
|
||
|