File picker permissions and header rename. Also merged jija shit

This commit is contained in:
nullpeer
2025-11-30 14:07:33 +03:00
parent d793498d0a
commit 803a855c21
18 changed files with 2820 additions and 1611 deletions

View File

@@ -0,0 +1,753 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:es_compression/lz4.dart';
import 'package:ffi/ffi.dart';
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
// FFI типы для LZ4 block decompress
typedef Lz4DecompressFunction =
Int32 Function(
Pointer<Uint8> src,
Pointer<Uint8> dst,
Int32 compressedSize,
Int32 dstCapacity,
);
typedef Lz4Decompress =
int Function(
Pointer<Uint8> src,
Pointer<Uint8> dst,
int compressedSize,
int dstCapacity,
);
class RegistrationService {
Socket? _socket;
int _seq = 0;
final Map<int, Completer<dynamic>> _pending = {};
bool _isConnected = false;
Timer? _pingTimer;
StreamSubscription? _socketSubscription;
Lz4Codec? _lz4Codec;
DynamicLibrary? _lz4Lib;
Lz4Decompress? _lz4BlockDecompress;
void _initLz4BlockDecompress() {
if (_lz4BlockDecompress != null) return;
try {
if (Platform.isWindows) {
// Пробуем загрузить eslz4-win64.dll
final dllPath = 'eslz4-win64.dll';
print('📦 Загрузка LZ4 DLL для block decompress: $dllPath');
_lz4Lib = DynamicLibrary.open(dllPath);
// Ищем функцию LZ4_decompress_safe (block format)
try {
_lz4BlockDecompress = _lz4Lib!
.lookup<NativeFunction<Lz4DecompressFunction>>(
'LZ4_decompress_safe',
)
.asFunction();
print('✅ LZ4 block decompress функция загружена');
} catch (e) {
print(
'⚠️ Функция LZ4_decompress_safe не найдена, пробуем альтернативные имена...',
);
// Пробуем другие возможные имена
try {
_lz4BlockDecompress = _lz4Lib!
.lookup<NativeFunction<Lz4DecompressFunction>>(
'LZ4_decompress_fast',
)
.asFunction();
print('✅ LZ4 block decompress функция загружена (fast)');
} catch (e2) {
print('Не удалось найти LZ4 block decompress функцию: $e2');
}
}
}
} catch (e) {
print('⚠️ Не удалось загрузить LZ4 DLL для block decompress: $e');
print('📦 Будем использовать только frame format (es_compression)');
}
}
Future<void> connect() async {
if (_isConnected) return;
// Инициализируем LZ4 block decompress
_initLz4BlockDecompress();
try {
print('🌐 Подключаемся к api.oneme.ru:443...');
// Создаем SSL контекст
final securityContext = SecurityContext.defaultContext;
print('🔒 Создаем TCP соединение...');
final rawSocket = await Socket.connect('api.oneme.ru', 443);
print('✅ TCP соединение установлено');
print('🔒 Устанавливаем SSL соединение...');
_socket = await SecureSocket.secure(
rawSocket,
context: securityContext,
host: 'api.oneme.ru',
onBadCertificate: (certificate) {
print('⚠️ Сертификат не прошел проверку, принимаем...');
return true;
},
);
_isConnected = true;
print('✅ SSL соединение установлено');
// Запускаем ping loop
_startPingLoop();
// Слушаем ответы
_socketSubscription = _socket!.listen(
_handleData,
onError: (error) {
print('❌ Ошибка сокета: $error');
_isConnected = false;
},
onDone: () {
print('🔌 Соединение закрыто');
_isConnected = false;
},
);
} catch (e) {
print('❌ Ошибка подключения: $e');
rethrow;
}
}
void _startPingLoop() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) async {
if (!_isConnected) {
timer.cancel();
return;
}
try {
await _sendMessage(1, {});
print('🏓 Ping отправлен');
} catch (e) {
print('❌ Ping failed: $e');
}
});
}
void _handleData(Uint8List data) {
// Обрабатываем данные по частям - сначала заголовок, потом payload
_processIncomingData(data);
}
Uint8List? _buffer = Uint8List(0);
void _processIncomingData(Uint8List newData) {
// Добавляем новые данные в буфер
_buffer = Uint8List.fromList([..._buffer!, ...newData]);
while (_buffer!.length >= 10) {
// Читаем заголовок
final header = _buffer!.sublist(0, 10);
final payloadLen =
ByteData.view(header.buffer, 6, 4).getUint32(0, Endian.big) &
0xFFFFFF;
if (_buffer!.length < 10 + payloadLen) {
// Недостаточно данных, ждем еще
break;
}
// Полный пакет готов
final fullPacket = _buffer!.sublist(0, 10 + payloadLen);
_buffer = _buffer!.sublist(10 + payloadLen);
_processPacket(fullPacket);
}
}
void _processPacket(Uint8List packet) {
try {
// Разбираем заголовок
final ver = packet[0];
final cmd = ByteData.view(packet.buffer).getUint16(1, Endian.big);
final seq = packet[3];
final opcode = ByteData.view(packet.buffer).getUint16(4, Endian.big);
final packedLen = ByteData.view(
packet.buffer,
6,
4,
).getUint32(0, Endian.big);
// Проверяем флаг сжатия (как в packet_framer.dart)
final compFlag = packedLen >> 24;
final payloadLen = packedLen & 0x00FFFFFF;
print('═══════════════════════════════════════════════════════════');
print('📥 ПОЛУЧЕН ПАКЕТ ОТ СЕРВЕРА');
print('═══════════════════════════════════════════════════════════');
print(
'📋 Заголовок: ver=$ver, cmd=$cmd, seq=$seq, opcode=$opcode, packedLen=$packedLen, compFlag=$compFlag, payloadLen=$payloadLen',
);
print('📦 Полный пакет (hex, ${packet.length} байт):');
print(_bytesToHex(packet));
print('');
final payloadBytes = packet.sublist(10, 10 + payloadLen);
print('📦 Сырые payload байты (hex, ${payloadBytes.length} байт):');
print(_bytesToHex(payloadBytes));
print('');
final payload = _unpackPacketPayload(payloadBytes, compFlag != 0);
print('📦 Разобранный payload (после LZ4 и msgpack):');
print(_formatPayload(payload));
print('═══════════════════════════════════════════════════════════');
print('');
// Находим completer по seq
final completer = _pending[seq];
if (completer != null && !completer.isCompleted) {
completer.complete(payload);
print('✅ Completer завершен для seq=$seq');
} else {
print('⚠️ Completer не найден для seq=$seq');
}
} catch (e) {
print('❌ Ошибка разбора пакета: $e');
print('Stack trace: ${StackTrace.current}');
}
}
Uint8List _packPacket(
int ver,
int cmd,
int seq,
int opcode,
Map<String, dynamic> payload,
) {
final verB = Uint8List(1)..[0] = ver;
final cmdB = Uint8List(2)
..buffer.asByteData().setUint16(0, cmd, Endian.big);
final seqB = Uint8List(1)..[0] = seq;
final opcodeB = Uint8List(2)
..buffer.asByteData().setUint16(0, opcode, Endian.big);
final payloadBytes = msgpack.serialize(payload);
final payloadLen = payloadBytes.length & 0xFFFFFF;
final payloadLenB = Uint8List(4)
..buffer.asByteData().setUint32(0, payloadLen, Endian.big);
final packet = Uint8List.fromList(
verB + cmdB + seqB + opcodeB + payloadLenB + payloadBytes,
);
print('═══════════════════════════════════════════════════════════');
print('📤 ОТПРАВЛЯЕМ ПАКЕТ НА СЕРВЕР');
print('═══════════════════════════════════════════════════════════');
print(
'📋 Заголовок: ver=$ver, cmd=$cmd, seq=$seq, opcode=$opcode, payloadLen=$payloadLen',
);
print('📦 Payload (JSON):');
print(_formatPayload(payload));
print('📦 Payload (msgpack hex, ${payloadBytes.length} байт):');
print(_bytesToHex(payloadBytes));
print('📦 Полный пакет (hex, ${packet.length} байт):');
print(_bytesToHex(packet));
print('═══════════════════════════════════════════════════════════');
print('');
return packet;
}
String _bytesToHex(Uint8List bytes) {
final buffer = StringBuffer();
for (int i = 0; i < bytes.length; i++) {
if (i > 0 && i % 16 == 0) buffer.writeln();
buffer.write(bytes[i].toRadixString(16).padLeft(2, '0').toUpperCase());
buffer.write(' ');
}
return buffer.toString();
}
String _formatPayload(dynamic payload) {
if (payload == null) return 'null';
if (payload is Map) {
final buffer = StringBuffer();
_formatMap(payload, buffer, 0);
return buffer.toString();
}
return payload.toString();
}
void _formatMap(Map map, StringBuffer buffer, int indent) {
final indentStr = ' ' * indent;
buffer.writeln('{');
map.forEach((key, value) {
buffer.write('$indentStr "$key": ');
if (value is Map) {
_formatMap(value, buffer, indent + 1);
} else if (value is List) {
buffer.writeln('[');
for (var item in value) {
buffer.write('$indentStr ');
if (item is Map) {
_formatMap(item, buffer, indent + 2);
} else {
buffer.writeln('$item,');
}
}
buffer.writeln('$indentStr ],');
} else {
buffer.writeln('$value,');
}
});
buffer.write('$indentStr}');
if (indent > 0) buffer.writeln(',');
}
dynamic _deserializeMsgpack(Uint8List data) {
print('📦 Десериализация msgpack...');
try {
final payload = msgpack.deserialize(data);
print('✅ Msgpack десериализация успешна');
// Проверяем, что получили валидный результат (не просто число)
if (payload is int && payload < 0) {
print(
'⚠️ Получено отрицательное число вместо Map - возможно данные все еще сжаты',
);
return null;
}
return payload;
} catch (e) {
print('❌ Ошибка десериализации msgpack: $e');
return null;
}
}
dynamic _unpackPacketPayload(
Uint8List payloadBytes, [
bool isCompressed = false,
]) {
if (payloadBytes.isEmpty) {
print('📦 Payload пустой');
return null;
}
try {
// Сначала пробуем LZ4 декомпрессию как в register.py
Uint8List decompressedBytes = payloadBytes;
// Если данные сжаты (compFlag != 0), пробуем LZ4 block декомпрессию
if (isCompressed && payloadBytes.length > 4) {
print('📦 Данные помечены как сжатые (compFlag != 0)');
// Пробуем LZ4 block декомпрессию через FFI (как в register.py)
try {
if (_lz4BlockDecompress != null) {
print('📦 Попытка LZ4 block декомпрессии через FFI...');
// В register.py используется фиксированный uncompressed_size=99999
// И данные используются полностью (без пропуска первых 4 байт)
// Но в packet_framer.dart при compFlag пропускаются первые 4 байта
// Попробуем оба варианта
// Вариант 1: как в register.py - используем все данные с фиксированным размером
// Увеличиваем размер для больших ответов (как в register.py используется 99999, но может быть недостаточно)
int uncompressedSize =
500000; // Увеличенный размер для больших ответов
Uint8List compressedData = payloadBytes;
print(
'📦 Попытка 1: Используем все данные с uncompressed_size=99999 (как в register.py)',
);
try {
if (uncompressedSize > 0 && uncompressedSize < 10 * 1024 * 1024) {
final srcSize = compressedData.length;
final srcPtr = malloc.allocate<Uint8>(srcSize);
final dstPtr = malloc.allocate<Uint8>(uncompressedSize);
try {
final srcList = srcPtr.asTypedList(srcSize);
srcList.setAll(0, compressedData);
final result = _lz4BlockDecompress!(
srcPtr,
dstPtr,
srcSize,
uncompressedSize,
);
if (result > 0) {
final actualSize = result;
final dstList = dstPtr.asTypedList(actualSize);
decompressedBytes = Uint8List.fromList(dstList);
print(
'✅ LZ4 block декомпрессия успешна: $srcSize${decompressedBytes.length} байт',
);
print(
'📦 Декомпрессированные данные (hex, первые 64 байта):',
);
final preview = decompressedBytes.length > 64
? decompressedBytes.sublist(0, 64)
: decompressedBytes;
print(_bytesToHex(preview));
// Успешная декомпрессия - возвращаем результат
return _deserializeMsgpack(decompressedBytes);
} else {
throw Exception('LZ4 декомпрессия вернула ошибку: $result');
}
} finally {
malloc.free(srcPtr);
malloc.free(dstPtr);
}
}
} catch (e1) {
print('⚠️ Вариант 1 не сработал: $e1');
// Вариант 2: пропускаем первые 4 байта (как в packet_framer.dart)
if (payloadBytes.length > 4) {
print('📦 Попытка 2: Пропускаем первые 4 байта...');
compressedData = payloadBytes.sublist(4);
print('📦 Сжатые данные (hex, первые 32 байта):');
final firstBytes = compressedData.length > 32
? compressedData.sublist(0, 32)
: compressedData;
print(_bytesToHex(firstBytes));
try {
final srcSize = compressedData.length;
final srcPtr = malloc.allocate<Uint8>(srcSize);
final dstPtr = malloc.allocate<Uint8>(uncompressedSize);
try {
final srcList = srcPtr.asTypedList(srcSize);
srcList.setAll(0, compressedData);
final result = _lz4BlockDecompress!(
srcPtr,
dstPtr,
srcSize,
uncompressedSize,
);
if (result > 0) {
final actualSize = result;
final dstList = dstPtr.asTypedList(actualSize);
decompressedBytes = Uint8List.fromList(dstList);
print(
'✅ LZ4 block декомпрессия успешна (вариант 2): $srcSize${decompressedBytes.length} байт',
);
print(
'📦 Декомпрессированные данные (hex, первые 64 байта):',
);
final preview = decompressedBytes.length > 64
? decompressedBytes.sublist(0, 64)
: decompressedBytes;
print(_bytesToHex(preview));
// Успешная декомпрессия - возвращаем результат
return _deserializeMsgpack(decompressedBytes);
} else {
throw Exception(
'LZ4 декомпрессия вернула ошибку: $result',
);
}
} finally {
malloc.free(srcPtr);
malloc.free(dstPtr);
}
} catch (e2) {
print('⚠️ Вариант 2 не сработал: $e2');
throw e2; // Пробрасываем ошибку дальше
}
} else {
throw e1;
}
}
} else {
// Пробуем через es_compression (frame format)
final compressedData = payloadBytes.sublist(4);
if (_lz4Codec == null) {
print('📦 Инициализация Lz4Codec (frame format)...');
_lz4Codec = Lz4Codec();
print('✅ Lz4Codec инициализирован успешно');
}
print('📦 Попытка декомпрессии через es_compression...');
final decoded = _lz4Codec!.decode(compressedData);
decompressedBytes = decoded is Uint8List
? decoded
: Uint8List.fromList(decoded);
print(
'✅ LZ4 декомпрессия успешна: ${compressedData.length}${decompressedBytes.length} байт',
);
}
} catch (lz4Error) {
print('⚠️ LZ4 декомпрессия не применена: $lz4Error');
print('📦 Тип ошибки: ${lz4Error.runtimeType}');
print('📦 Используем сырые данные...');
decompressedBytes = payloadBytes;
}
} else {
// Данные не сжаты или нет флага - пробуем LZ4 на всякий случай (как в register.py)
print(
'📦 Данные не помечены как сжатые, но пробуем LZ4 (как в register.py)...',
);
final firstBytes = payloadBytes.length > 32
? payloadBytes.sublist(0, 32)
: payloadBytes;
print(
'📦 Первые ${firstBytes.length} байта payload (hex): ${_bytesToHex(firstBytes)}',
);
try {
if (_lz4Codec == null) {
print('📦 Инициализация Lz4Codec...');
_lz4Codec = Lz4Codec();
print('✅ Lz4Codec инициализирован успешно');
}
print('📦 Попытка декомпрессии ${payloadBytes.length} байт...');
final decoded = _lz4Codec!.decode(payloadBytes);
decompressedBytes = decoded is Uint8List
? decoded
: Uint8List.fromList(decoded);
print(
'✅ LZ4 декомпрессия успешна: ${payloadBytes.length}${decompressedBytes.length} байт',
);
} catch (lz4Error) {
// Если LZ4 не удалась (данные не сжаты), используем сырые данные
print(
'⚠️ LZ4 декомпрессия не применена (данные не сжаты): $lz4Error',
);
decompressedBytes = payloadBytes;
}
}
return _deserializeMsgpack(decompressedBytes);
} catch (e) {
print('❌ Ошибка десериализации payload: $e');
print('Stack trace: ${StackTrace.current}');
return null;
}
}
Future<dynamic> _sendMessage(int opcode, Map<String, dynamic> payload) async {
if (!_isConnected || _socket == null) {
throw Exception('Не подключено к серверу');
}
_seq = (_seq + 1) % 256;
final seq = _seq;
final packet = _packPacket(10, 0, seq, opcode, payload);
print('📤 Отправляем сообщение opcode=$opcode, seq=$seq');
final completer = Completer<dynamic>();
_pending[seq] = completer;
_socket!.add(packet);
await _socket!.flush();
return completer.future.timeout(const Duration(seconds: 30));
}
Future<String> startRegistration(String phoneNumber) async {
await connect();
// Отправляем handshake
final handshakePayload = {
"mt_instanceid": "63ae21a8-2417-484d-849b-0ae464a7b352",
"userAgent": {
"deviceType": "ANDROID",
"appVersion": "25.14.2",
"osVersion": "Android 14",
"timezone": "Europe/Moscow",
"screen": "440dpi 440dpi 1080x2072",
"pushDeviceType": "GCM",
"arch": "x86_64",
"locale": "ru",
"buildNumber": 6442,
"deviceName": "unknown Android SDK built for x86_64",
"deviceLocale": "en",
},
"clientSessionId": 8,
"deviceId": "d53058ab998c3bdd",
};
print('🤝 Отправляем handshake (opcode=6)...');
print('📦 Handshake payload:');
print(_formatPayload(handshakePayload));
final handshakeResponse = await _sendMessage(6, handshakePayload);
print('📨 Ответ от handshake:');
print(_formatPayload(handshakeResponse));
// Проверяем ошибки
if (handshakeResponse is Map) {
final err = handshakeResponse['payload']?['error'];
if (err != null) {
print('❌ Ошибка handshake: $err');
}
}
// Отправляем START_AUTH
final authPayload = {"type": "START_AUTH", "phone": phoneNumber};
print('🚀 Отправляем START_AUTH (opcode=17)...');
print('📦 START_AUTH payload:');
print(_formatPayload(authPayload));
final response = await _sendMessage(17, authPayload);
print('📨 Ответ от START_AUTH:');
print(_formatPayload(response));
// Проверяем ошибки
if (response is Map) {
// Проверяем ошибку в payload или в корне ответа
final payload = response['payload'] ?? response;
final err = payload['error'] ?? response['error'];
if (err != null) {
// Обрабатываем конкретную ошибку limit.violate
if (err.toString().contains('limit.violate') ||
err.toString().contains('error.limit.violate')) {
throw Exception(
'У вас кончились попытки на код, попробуйте позже...',
);
}
// Для других ошибок используем сообщение от сервера или общее
final message =
payload['localizedMessage'] ??
payload['message'] ??
payload['description'] ??
'Ошибка START_AUTH: $err';
throw Exception(message);
}
}
// Извлекаем токен из ответа (как в register.py)
if (response is Map) {
final payload = response['payload'] ?? response;
final token = payload['token'] ?? response['token'];
if (token != null) {
return token as String;
}
}
throw Exception('Не удалось получить токен из ответа сервера');
}
Future<String> verifyCode(String token, String code) async {
final verifyPayload = {
"verifyCode": code,
"token": token,
"authTokenType": "CHECK_CODE",
};
print('🔍 Проверяем код (opcode=18)...');
print('📦 CHECK_CODE payload:');
print(_formatPayload(verifyPayload));
final response = await _sendMessage(18, verifyPayload);
print('📨 Ответ от CHECK_CODE:');
print(_formatPayload(response));
// Проверяем ошибки
if (response is Map) {
// Проверяем ошибку в payload или в корне ответа
final payload = response['payload'] ?? response;
final err = payload['error'] ?? response['error'];
if (err != null) {
// Обрабатываем конкретную ошибку неправильного кода
if (err.toString().contains('verify.code.wrong') ||
err.toString().contains('wrong.code') ||
err.toString().contains('code.wrong')) {
throw Exception('Неверный код');
}
// Для других ошибок используем сообщение от сервера или общее
final message =
payload['localizedMessage'] ??
payload['message'] ??
payload['title'] ??
'Ошибка CHECK_CODE: $err';
throw Exception(message);
}
}
// Извлекаем register токен (как в register.py)
if (response is Map) {
final tokenSrc = response['payload'] ?? response;
final tokenAttrs = tokenSrc['tokenAttrs'];
// Проверяем, есть ли LOGIN токен - значит аккаунт уже существует
if (tokenAttrs is Map && tokenAttrs['LOGIN'] is Map) {
throw Exception('ACCOUNT_EXISTS');
}
if (tokenAttrs is Map && tokenAttrs['REGISTER'] is Map) {
final registerToken = tokenAttrs['REGISTER']['token'];
if (registerToken != null) {
return registerToken as String;
}
}
}
throw Exception('Не удалось получить токен регистрации из ответа сервера');
}
Future<void> completeRegistration(String registerToken) async {
final registerPayload = {
"lastName": "User",
"token": registerToken,
"firstName": "Komet",
"tokenType": "REGISTER",
};
print('🎉 Завершаем регистрацию (opcode=23)...');
print('📦 REGISTER payload:');
print(_formatPayload(registerPayload));
final response = await _sendMessage(23, registerPayload);
print('📨 Ответ от REGISTER:');
print(_formatPayload(response));
// Проверяем ошибки
if (response is Map) {
final err = response['payload']?['error'];
if (err != null) {
throw Exception('Ошибка REGISTER: $err');
}
// Извлекаем финальный токен
final payload = response['payload'] ?? response;
final finalToken = payload['token'] ?? response['token'];
if (finalToken != null) {
print('✅ Регистрация успешна, финальный токен: $finalToken');
return;
}
}
throw Exception('Регистрация не удалась');
}
void disconnect() {
try {
_isConnected = false;
_pingTimer?.cancel();
_socketSubscription?.cancel();
_socket?.close();
print('🔌 Отключено от сервера');
} catch (e) {
print('❌ Ошибка отключения: $e');
}
}
}

View File

@@ -255,4 +255,113 @@ extension ApiServiceAuth on ApiService {
rethrow;
}
}
// Registration methods
Future<String> startRegistration(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",
};
// Listen for the response
final completer = Completer<Map<String, dynamic>>();
final subscription = messages.listen((message) {
if (message['opcode'] == 17 && !completer.isCompleted) {
completer.complete(message);
}
});
_sendMessage(17, payload);
try {
final response = await completer.future.timeout(const Duration(seconds: 30));
subscription.cancel();
final payload = response['payload'];
if (payload != null && payload['token'] != null) {
return payload['token'];
} else {
throw Exception('No registration token received');
}
} catch (e) {
subscription.cancel();
rethrow;
}
}
Future<String> verifyRegistrationCode(String token, String code) async {
final payload = {
'token': token,
'verifyCode': code,
'authTokenType': 'CHECK_CODE',
};
final completer = Completer<Map<String, dynamic>>();
final subscription = messages.listen((message) {
if (message['opcode'] == 18 && !completer.isCompleted) {
completer.complete(message);
}
});
_sendMessage(18, payload);
try {
final response = await completer.future.timeout(const Duration(seconds: 30));
subscription.cancel();
final payload = response['payload'];
if (payload != null) {
final tokenAttrs = payload['tokenAttrs'];
if (tokenAttrs != null && tokenAttrs['REGISTER'] != null) {
final regToken = tokenAttrs['REGISTER']['token'];
if (regToken != null) {
return regToken;
}
}
}
throw Exception('Registration token not found in response');
} catch (e) {
subscription.cancel();
rethrow;
}
}
Future<void> completeRegistration(String regToken) async {
final payload = {
"lastName": "User",
"token": regToken,
"firstName": "Komet",
"tokenType": "REGISTER",
};
final completer = Completer<Map<String, dynamic>>();
final subscription = messages.listen((message) {
if (message['opcode'] == 23 && !completer.isCompleted) {
completer.complete(message);
}
});
_sendMessage(23, payload);
try {
await completer.future.timeout(const Duration(seconds: 30));
subscription.cancel();
print('Registration completed successfully');
} catch (e) {
subscription.cancel();
rethrow;
}
}
}

View File

@@ -16,6 +16,12 @@ extension ApiServiceContacts on ApiService {
_sendMessage(34, {'contactId': contactId, 'action': 'ADD'});
}
Future<void> requestContactsByIds(List<int> contactIds) async {
await waitUntilOnline();
_sendMessage(35, {'contactIds': contactIds});
print('Отправлен запрос opcode=35 с contactIds: $contactIds');
}
Future<void> subscribeToChat(int chatId, bool subscribe) async {
await waitUntilOnline();
_sendMessage(75, {'chatId': chatId, 'subscribe': subscribe});