FUCKING REFACTOR

This commit is contained in:
ivan2282
2025-11-19 18:45:44 +03:00
parent 2d11f1cba2
commit 575c43ce63
41 changed files with 2915 additions and 2910 deletions

219
lib/api/api_service.dart Normal file
View File

@@ -0,0 +1,219 @@
library api_service;
import 'dart:async';
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.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/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';
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<String> _wsUrls = ['wss://ws-api.oneme.ru:443/websocket'];
int _currentUrlIndex = 0;
List<String> get wsUrls => _wsUrls;
int get currentUrlIndex => _currentUrlIndex;
IOWebSocketChannel? _channel;
StreamSubscription? _streamSubscription;
Timer? _pingTimer;
int _seq = 0;
final StreamController<Contact> _contactUpdatesController =
StreamController<Contact>.broadcast();
Stream<Contact> get contactUpdates => _contactUpdatesController.stream;
final StreamController<String> _errorController =
StreamController<String>.broadcast();
Stream<String> get errorStream => _errorController.stream;
final _reconnectionCompleteController = StreamController<void>.broadcast();
Stream<void> get reconnectionComplete =>
_reconnectionCompleteController.stream;
final Map<String, dynamic> _presenceData = {};
String? authToken;
String? userId;
String? get token => authToken;
String? _currentPasswordTrackId;
String? _currentPasswordHint;
String? _currentPasswordEmail;
bool _isSessionOnline = false;
bool _handshakeSent = false;
Completer<void>? _onlineCompleter;
final List<Map<String, dynamic>> _messageQueue = [];
final Map<int, List<Message>> _messageCache = {};
final Map<int, Contact> _contactCache = {};
DateTime? _lastContactsUpdate;
static const Duration _contactCacheExpiry = Duration(
minutes: 5,
);
bool _isLoadingBlockedContacts = false;
bool _isSessionReady = false;
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get messages => _messageController.stream;
final _connectionStatusController = StreamController<String>.broadcast();
Stream<String> get connectionStatus => _connectionStatusController.stream;
final _connectionLogController = StreamController<String>.broadcast();
Stream<String> get connectionLog => _connectionLogController.stream;
final List<String> _connectionLogCache = [];
List<String> get connectionLogCache => _connectionLogCache;
bool get isOnline => _isSessionOnline;
Future<void> waitUntilOnline() async {
if (_isSessionOnline && _isSessionReady) return;
_onlineCompleter ??= Completer<void>();
return _onlineCompleter!.future;
}
bool get isActuallyConnected {
try {
if (_channel == null || !_isSessionOnline) {
return false;
}
return true;
} catch (e) {
print("🔴 Ошибка при проверке состояния канала: $e");
return false;
}
}
Completer<Map<String, dynamic>>? _inflightChatsCompleter;
Map<String, dynamic>? _lastChatsPayload;
DateTime? _lastChatsAt;
final Duration _chatsCacheTtl = const Duration(seconds: 5);
bool _chatsFetchedInThisSession = false;
Map<String, dynamic>? get lastChatsPayload => _lastChatsPayload;
int _reconnectDelaySeconds = 2;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 10;
Timer? _reconnectTimer;
bool _isReconnecting = false;
void _log(String message) {
print(message);
_connectionLogCache.add(message);
if (!_connectionLogController.isClosed) {
_connectionLogController.add(message);
}
}
void _emitLocal(Map<String, dynamic> frame) {
try {
_messageController.add(frame);
} catch (_) {}
}
String generateRandomDeviceId() {
return const Uuid().v4();
}
Future<Map<String, dynamic>> _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;
}
Future<String?> 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();
}
}

View File

@@ -0,0 +1,260 @@
part of 'api_service.dart';
extension ApiServiceAuth on ApiService {
Future<void> _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<void> 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<void> 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<void> sendPassword(String trackId, String password) async {
await waitUntilOnline();
final payload = {'trackId': trackId, 'password': password};
_sendMessage(115, payload);
print('Пароль отправлен с payload: $payload');
}
Map<String, String?> getPasswordAuthData() {
return {
'trackId': _currentPasswordTrackId,
'hint': _currentPasswordHint,
'email': _currentPasswordEmail,
};
}
void clearPasswordAuthData() {
_currentPasswordTrackId = null;
_currentPasswordHint = null;
_currentPasswordEmail = null;
}
Future<void> setAccountPassword(String password, String hint) async {
await waitUntilOnline();
final payload = {'password': password, 'hint': hint};
_sendMessage(116, payload);
print('Запрос на установку пароля отправлен с payload: $payload');
}
Future<void> saveToken(
String token, {
String? userId,
Profile? profile,
}) async {
print("Сохраняем новый токен: ${token.substring(0, 20)}...");
if (userId != null) {
print("Сохраняем UserID: $userId");
}
final accountManager = AccountManager();
await accountManager.initialize();
final account = await accountManager.addAccount(
token: token,
userId: userId,
profile: profile,
);
await accountManager.switchAccount(account.id);
authToken = token;
this.userId = userId;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('authToken', token);
if (userId != null) {
await prefs.setString('userId', userId);
}
disconnect();
await connect();
await getChatsAndContacts(force: true);
print("Токен и UserID успешно сохранены");
}
Future<bool> hasToken() async {
if (authToken == null) {
final accountManager = AccountManager();
await accountManager.initialize();
await accountManager.migrateOldAccount();
final currentAccount = accountManager.currentAccount;
if (currentAccount != null) {
authToken = currentAccount.token;
userId = currentAccount.userId;
print(
"Токен загружен из AccountManager: ${authToken!.substring(0, 20)}...",
);
} else {
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<void> _loadTokenFromAccountManager() async {
final accountManager = AccountManager();
await accountManager.initialize();
final currentAccount = accountManager.currentAccount;
if (currentAccount != null) {
authToken = currentAccount.token;
userId = currentAccount.userId;
}
}
Future<void> switchAccount(String accountId) async {
print("Переключение на аккаунт: $accountId");
disconnect();
final accountManager = AccountManager();
await accountManager.initialize();
await accountManager.switchAccount(accountId);
final currentAccount = accountManager.currentAccount;
if (currentAccount != null) {
authToken = currentAccount.token;
userId = currentAccount.userId;
_messageCache.clear();
_messageQueue.clear();
_lastChatsPayload = null;
_chatsFetchedInThisSession = false;
_isSessionOnline = false;
_isSessionReady = false;
_handshakeSent = false;
await connect();
await waitUntilOnline();
await getChatsAndContacts(force: true);
final profile = _lastChatsPayload?['profile'];
if (profile != null) {
final profileObj = Profile.fromJson(profile);
await accountManager.updateAccountProfile(accountId, profileObj);
}
}
}
Future<void> 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<void> 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;
}
}
}

View File

@@ -0,0 +1,861 @@
part of 'api_service.dart';
extension ApiServiceChats on ApiService {
void createGroup(String name, List<int> participantIds) {
final payload = {"name": name, "participantIds": participantIds};
_sendMessage(48, payload);
print('Создаем группу: $name с участниками: $participantIds');
}
void updateGroup(int chatId, {String? name, List<int>? 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<int> 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<int> 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<int> 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<Map<String, dynamic>> getChatsOnly({bool force = false}) async {
if (authToken == null) {
await _loadTokenFromAccountManager();
}
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<dynamic> chatListJson =
chatResponse['payload']?['chats'] ?? [];
if (chatListJson.isEmpty) {
final result = {'chats': [], 'contacts': [], 'profile': null};
_lastChatsPayload = result;
_lastChatsAt = DateTime.now();
return result;
}
final contactIds = <int>{};
for (var chatJson in chatListJson) {
final participants =
chatJson['participants'] as Map<String, dynamic>? ?? {};
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<dynamic> 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<Map<String, dynamic>> 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<Map<String, dynamic>>();
if (_isSessionOnline &&
_isSessionReady &&
_lastChatsPayload != null &&
!force) {
_inflightChatsCompleter!.complete(_lastChatsPayload!);
_inflightChatsCompleter = null;
return _lastChatsPayload!;
}
try {
Map<String, dynamic> chatResponse;
final int opcode;
final Map<String, dynamic> 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<dynamic> chatListJson =
chatResponse['payload']?['chats'] ?? [];
if (profile != null && authToken != null) {
try {
final accountManager = AccountManager();
await accountManager.initialize();
final currentAccount = accountManager.currentAccount;
if (currentAccount != null && currentAccount.token == authToken) {
final profileObj = Profile.fromJson(profile);
await accountManager.updateAccountProfile(
currentAccount.id,
profileObj,
);
}
} catch (e) {
print('Ошибка сохранения профиля в AccountManager: $e');
}
}
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 = <int>{};
for (var chatJson in chatListJson) {
final participants = chatJson['participants'] as Map<String, dynamic>;
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<dynamic> 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<void> _sendInitialSetupRequests() async {
print("Запускаем отправку единичных запросов при старте...");
if (!_isSessionOnline || !_isSessionReady) {
print("Сессия еще не готова, ждем...");
await waitUntilOnline();
}
await Future.delayed(const Duration(seconds: 2));
if (!_isSessionOnline || !_isSessionReady) {
print("Сессия не готова для отправки запросов, пропускаем");
return;
}
_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("Единичные запросы отправлены.");
}
Future<List<Message>> 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<dynamic> 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<Map<String, dynamic>?> 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 sendNavEvent(String event, {int? screenTo, int? screenFrom}) {
if (_userId == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
final Map<String, dynamic> 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;
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<int>? include,
List<dynamic>? 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<int>? include,
List<dynamic>? 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');
}
void deleteFolder(String folderId) {
final payload = {
"folderIds": [folderId],
};
_sendMessage(276, payload);
print('Удаляем папку: $folderId');
}
void requestFolderSync() {
_sendMessage(272, {"folderSync": 0});
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<Map<String, dynamic>> getNetworkStatistics() async {
final prefs = await SharedPreferences.getInstance();
final totalTraffic =
prefs.getDouble('network_total_traffic') ??
(150.0 * 1024 * 1024);
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;
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!) <
ApiService._contactCacheExpiry;
}
void updateContactCache(List<Contact> 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("Все кэши очищены из-за ошибки подключения.");
}
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});
}
}
Future<void> 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<bool> 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<void> 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<bool> 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);
}
}
}

View File

@@ -0,0 +1,719 @@
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");
}
}

View File

@@ -0,0 +1,316 @@
part of 'api_service.dart';
extension ApiServiceContacts on ApiService {
Future<void> blockContact(int contactId) async {
await waitUntilOnline();
_sendMessage(34, {'contactId': contactId, 'action': 'BLOCK'});
}
Future<void> unblockContact(int contactId) async {
await waitUntilOnline();
_sendMessage(34, {'contactId': contactId, 'action': 'UNBLOCK'});
}
Future<void> addContact(int contactId) async {
await waitUntilOnline();
_sendMessage(34, {'contactId': contactId, 'action': 'ADD'});
}
Future<void> subscribeToChat(int chatId, bool subscribe) async {
await waitUntilOnline();
_sendMessage(75, {'chatId': chatId, 'subscribe': subscribe});
}
Future<void> navigateToChat(int currentChatId, int targetChatId) async {
await waitUntilOnline();
if (currentChatId != 0) {
await subscribeToChat(currentChatId, false);
}
await subscribeToChat(targetChatId, true);
}
Future<void> clearChatHistory(int chatId, {bool forAll = false}) async {
await waitUntilOnline();
final payload = {
'chatId': chatId,
'forAll': forAll,
'lastEventTime': DateTime.now().millisecondsSinceEpoch,
};
_sendMessage(54, payload);
}
Future<Map<String, dynamic>> 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<String, dynamic>;
} 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<String, dynamic> 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');
}
Future<Map<String, dynamic>> 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<String, dynamic>;
} else {
print('Неожиданный ответ на joinGroupByLink: $response');
throw Exception('Неверный ответ от сервера');
}
} on TimeoutException {
print('Таймаут ожидания ответа на joinGroupByLink (seq: $seq)');
throw Exception('Сервер не ответил вовремя');
} catch (e) {
print('Ошибка в joinGroupByLink: $e');
rethrow;
}
}
Future<void> searchContactByPhone(String phone) async {
await waitUntilOnline();
final payload = {'phone': phone};
_sendMessage(46, payload);
print('Запрос на поиск контакта отправлен с payload: $payload');
}
Future<void> searchChannels(String query) async {
await waitUntilOnline();
final payload = {'contactIds': []};
_sendMessage(32, payload);
print('Запрос на поиск каналов отправлен с payload: $payload');
}
Future<void> enterChannel(String link) async {
await waitUntilOnline();
final payload = {'link': link};
_sendMessage(89, payload);
print('Запрос на вход в канал отправлен с payload: $payload');
}
Future<void> subscribeToChannel(String link) async {
await waitUntilOnline();
final payload = {'link': link};
_sendMessage(57, payload);
print('Запрос на подписку на канал отправлен с payload: $payload');
}
Future<int?> 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<dynamic>?;
if (chats != null && chats.isNotEmpty) {
final chat = chats[0] as Map<String, dynamic>;
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<List<Contact>> fetchContactsByIds(List<int> 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<dynamic> 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 [];
}
}
}

View File

@@ -0,0 +1,371 @@
part of 'api_service.dart';
extension ApiServiceMedia on ApiService {
void updateProfileText(
String firstName,
String lastName,
String description,
) {
final payload = {
"firstName": firstName,
"lastName": lastName,
"description": description,
};
_sendMessage(16, payload);
}
Future<void> 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<void> sendPhotoMessage(
int chatId, {
String? localPath,
String? caption,
int? cidOverride,
int? senderId,
}) 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<String, dynamic>;
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<void> sendPhotoMessages(
int chatId, {
required List<String> 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<Map<String, String>> 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<String, dynamic>;
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<void> sendFileMessage(
int chatId, {
String? caption,
int? senderId,
}) 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},
],
},
"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',
},
],
},
},
});
_sendMessage(64, payload);
print('Сообщение о файле (Opcode 64) отправлено.');
} catch (e) {
print('Ошибка отправки файла: $e');
}
}
Future<String> 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<String, dynamic>?;
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;
}
}
}

View File

@@ -0,0 +1,133 @@
part of 'api_service.dart';
extension ApiServicePrivacy on ApiService {
Future<void> updatePrivacySettings({
String? hidden,
String? searchByPhone,
String? incomingCall,
String? chatsInvite,
bool? chatsPushNotification,
String? chatsPushSound,
String? pushSound,
bool? mCallPushNotification,
bool? pushDetails,
bool? contentLevelAccess,
}) async {
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<void> _updateSinglePrivacySetting(Map<String, dynamic> setting) async {
await waitUntilOnline();
final payload = {
'settings': {'user': setting},
};
_sendMessage(22, payload);
print('');
}
Future<void> _updatePrivacySettingWithSeq(
Map<String, dynamic> 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<String, dynamic>? config) {
if (config == null) return;
final userConfig = config['user'] as Map<String, dynamic>?;
if (userConfig == null) return;
print('Обработка настроек приватности с сервера: $userConfig');
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},
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/channel.dart';
import 'package:gwid/search_channels_screen.dart';

View File

@@ -6,7 +6,7 @@ import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:flutter/services.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/models/message.dart';

View File

@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:gwid/chat_screen.dart';
import 'package:gwid/manage_account_screen.dart';

View File

@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'theme_provider.dart';
class ConnectionLifecycleManager extends StatefulWidget {

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
class RequestHistoryItem {

View File

@@ -4,7 +4,7 @@ import 'package:flutter/scheduler.dart';
import 'package:gwid/cache_management_screen.dart'; // Добавлен импорт
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/phone_entry_screen.dart';
import 'package:gwid/custom_request_screen.dart';
import 'dart:async';

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/chats_screen.dart';
import 'package:gwid/phone_entry_screen.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/screens/settings/reconnection_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/services/version_checker.dart';

View File

@@ -2,7 +2,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
class JoinGroupScreen extends StatefulWidget {
const JoinGroupScreen({super.key});

View File

@@ -9,7 +9,7 @@ import 'theme_provider.dart';
import 'package:provider/provider.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'connection_lifecycle_manager.dart';
import 'services/cache_service.dart';
import 'services/avatar_cache_service.dart';

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/phone_entry_screen.dart';
import 'package:image_picker/image_picker.dart';

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:pinput/pinput.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/chats_screen.dart';
import 'package:gwid/password_auth_screen.dart';

View File

@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/chats_screen.dart';
class PasswordAuthScreen extends StatefulWidget {

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
class PasswordManagementScreen extends StatefulWidget {
const PasswordManagementScreen({super.key});

View File

@@ -3,7 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/otp_screen.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/screens/settings/proxy_settings_screen.dart';

View File

@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/widgets/user_profile_panel.dart';

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/spoofing_service.dart';
import 'package:encrypt/encrypt.dart' as encrypt;

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
class NetworkScreen extends StatefulWidget {
const NetworkScreen({super.key});

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'dart:io' show Platform;
class NotificationSettingsScreen extends StatefulWidget {

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/theme_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@@ -3,7 +3,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'dart:convert';
class QrLoginScreen extends StatefulWidget {

View File

@@ -1,6 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/home_screen.dart';
class ReconnectionScreen extends StatefulWidget {

View File

@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:uuid/uuid.dart';
import 'package:gwid/device_presets.dart';
import 'package:gwid/phone_entry_screen.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'dart:async';
import 'package:intl/intl.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/manage_account_screen.dart';
import 'package:gwid/screens/settings/appearance_settings_screen.dart';
import 'package:gwid/screens/settings/notification_settings_screen.dart';

View File

@@ -4,7 +4,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';

View File

@@ -2,7 +2,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/channel.dart';
class SearchChannelsScreen extends StatefulWidget {

View File

@@ -2,7 +2,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
class SearchContactScreen extends StatefulWidget {

View File

@@ -10,7 +10,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/home_screen.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/proxy_settings.dart';

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
class UserIdLookupScreen extends StatefulWidget {

View File

@@ -17,7 +17,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gwid/chat_screen.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:open_file/open_file.dart';

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:gwid/models/chat.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/screens/group_settings_screen.dart';
class GroupManagementPanel extends StatefulWidget {

View File

@@ -4,7 +4,7 @@ import 'package:gwid/models/chat.dart';
import 'package:gwid/models/message.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/api_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/widgets/chat_message_bubble.dart';
import 'package:gwid/chat_screen.dart';