починил рег, кто так смержил уебищно скажите мне???? Добавил кнопку создание приглосительной ссылки в чат. Больше пока ничего не добавляем - жду пока дед запушит и займусь багофиксом, если дед все не закроет
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:es_compression/lz4.dart';
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
|
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
|
||||||
|
|
||||||
@@ -29,7 +28,9 @@ class RegistrationService {
|
|||||||
bool _isConnected = false;
|
bool _isConnected = false;
|
||||||
Timer? _pingTimer;
|
Timer? _pingTimer;
|
||||||
StreamSubscription? _socketSubscription;
|
StreamSubscription? _socketSubscription;
|
||||||
Lz4Codec? _lz4Codec;
|
// LZ4 через es_compression/FFI сейчас не работает на Windows из‑за отсутствия
|
||||||
|
// eslz4-win64.dll, поэтому ниже реализован свой чистый декодер LZ4 block.
|
||||||
|
// Поля для LZ4 через FFI оставлены на будущее, если появится корректная DLL.
|
||||||
DynamicLibrary? _lz4Lib;
|
DynamicLibrary? _lz4Lib;
|
||||||
Lz4Decompress? _lz4BlockDecompress;
|
Lz4Decompress? _lz4BlockDecompress;
|
||||||
|
|
||||||
@@ -315,19 +316,193 @@ class RegistrationService {
|
|||||||
dynamic _deserializeMsgpack(Uint8List data) {
|
dynamic _deserializeMsgpack(Uint8List data) {
|
||||||
print('📦 Десериализация msgpack...');
|
print('📦 Десериализация msgpack...');
|
||||||
try {
|
try {
|
||||||
final payload = msgpack.deserialize(data);
|
dynamic payload = msgpack.deserialize(data);
|
||||||
print('✅ Msgpack десериализация успешна');
|
print('✅ Msgpack десериализация успешна');
|
||||||
|
|
||||||
// Проверяем, что получили валидный результат (не просто число)
|
// Иногда сервер шлёт FFI‑токены в виде "отрицательное число + настоящий объект"
|
||||||
if (payload is int && payload < 0) {
|
// в одном msgpack‑буфере. msgpack_dart в таком случае возвращает только первое
|
||||||
|
// значение (например, -16 или -13), а остальное игнорирует.
|
||||||
|
//
|
||||||
|
// Паттерны из логов:
|
||||||
|
// - F0 56 84 ... → -16 и дальше полноценная map
|
||||||
|
// - F3 A7 85 ... → -13 и дальше полноценная map
|
||||||
|
//
|
||||||
|
// Если мы увидели отрицательный fixint и в буфере есть ещё данные,
|
||||||
|
// пробуем повторно распарсить "хвост" как настоящий payload.
|
||||||
|
if (payload is int && data.length > 1 && payload <= -1 && payload >= -32) {
|
||||||
|
final marker = data[0];
|
||||||
|
|
||||||
|
// Для разных FFI‑токенов offset до реального msgpack может отличаться.
|
||||||
|
// Вместо жёсткой привязки пробуем несколько вариантов подряд.
|
||||||
|
final candidateOffsets = <int>[1, 2, 3, 4];
|
||||||
|
|
||||||
|
// Сохраним сюда первый успешно распарсенный payload.
|
||||||
|
dynamic recovered;
|
||||||
|
|
||||||
|
for (final offset in candidateOffsets) {
|
||||||
|
if (offset >= data.length) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
print(
|
print(
|
||||||
'⚠️ Получено отрицательное число вместо Map - возможно данные все еще сжаты',
|
'📦 Обнаружен FFI‑токен $payload (marker=0x${marker.toRadixString(16)}), '
|
||||||
|
'пробуем msgpack c offset=$offset...',
|
||||||
|
);
|
||||||
|
final tail = data.sublist(offset);
|
||||||
|
final realPayload = msgpack.deserialize(tail);
|
||||||
|
print('✅ Удалось распарсить payload после FFI‑токена с offset=$offset');
|
||||||
|
recovered = realPayload;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'⚠️ Попытка распарсить хвост msgpack (offset=$offset) не удалась: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recovered != null) {
|
||||||
|
payload = recovered;
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'⚠️ Не удалось восстановить payload после FFI‑токена, '
|
||||||
|
'оставляем исходное значение ($payload).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// После базовой (и возможной повторной) десериализации дополнительно
|
||||||
|
// разбираем "block"-объекты — структуры с lz4‑сжатыми данными.
|
||||||
|
final decoded = _decodeBlockTokens(payload);
|
||||||
|
return decoded;
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка десериализации msgpack: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Рекурсивно обходит структуру ответа и декодирует блоки вида:
|
||||||
|
/// {"type": "block", "data": <bytes>, "uncompressed_size": N}
|
||||||
|
/// Такие блоки используются FFI для передачи lz4‑сжатых кусков данных.
|
||||||
|
dynamic _decodeBlockTokens(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
// Пытаемся декодировать саму map как block‑токен
|
||||||
|
final maybeDecoded = _tryDecodeSingleBlock(value);
|
||||||
|
if (maybeDecoded != null) {
|
||||||
|
return maybeDecoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это обычная map — обходим все поля рекурсивно
|
||||||
|
final result = <dynamic, dynamic>{};
|
||||||
|
value.forEach((k, v) {
|
||||||
|
result[k] = _decodeBlockTokens(v);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} else if (value is List) {
|
||||||
|
return value.map(_decodeBlockTokens).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Пробует интерпретировать map как блок вида "block".
|
||||||
|
/// Если структура не похожа на блок, возвращает null.
|
||||||
|
dynamic _tryDecodeSingleBlock(Map value) {
|
||||||
|
try {
|
||||||
|
if (value['type'] != 'block') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rawData = value['data'];
|
||||||
|
if (rawData is! List && rawData is! Uint8List) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пробуем вытащить ожидаемый размер распакованных данных.
|
||||||
|
// Название поля может отличаться, поэтому проверяем несколько вариантов.
|
||||||
|
final uncompressedSize = (value['uncompressed_size'] ??
|
||||||
|
value['uncompressedSize'] ??
|
||||||
|
value['size']) as int?;
|
||||||
|
|
||||||
|
Uint8List compressedBytes = rawData is Uint8List
|
||||||
|
? rawData
|
||||||
|
: Uint8List.fromList(List<int>.from(rawData as List));
|
||||||
|
|
||||||
|
// Если FFI‑функция доступна — используем её (LZ4_decompress_safe).
|
||||||
|
if (_lz4BlockDecompress != null && uncompressedSize != null) {
|
||||||
|
print(
|
||||||
|
'📦 Декодируем block‑токен через LZ4 FFI: '
|
||||||
|
'compressed=${compressedBytes.length}, uncompressed=$uncompressedSize',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uncompressedSize <= 0 || uncompressedSize > 10 * 1024 * 1024) {
|
||||||
|
print(
|
||||||
|
'⚠️ Некорректный uncompressed_size=$uncompressedSize, '
|
||||||
|
'пропускаем FFI‑декомпрессию для этого блока',
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return payload;
|
|
||||||
|
final srcSize = compressedBytes.length;
|
||||||
|
final srcPtr = malloc.allocate<Uint8>(srcSize);
|
||||||
|
final dstPtr = malloc.allocate<Uint8>(uncompressedSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final srcList = srcPtr.asTypedList(srcSize);
|
||||||
|
srcList.setAll(0, compressedBytes);
|
||||||
|
|
||||||
|
final result = _lz4BlockDecompress!(
|
||||||
|
srcPtr,
|
||||||
|
dstPtr,
|
||||||
|
srcSize,
|
||||||
|
uncompressedSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result <= 0) {
|
||||||
|
print('❌ LZ4_decompress_safe вернула код ошибки: $result');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final actualSize = result;
|
||||||
|
final dstList = dstPtr.asTypedList(actualSize);
|
||||||
|
final decompressed = Uint8List.fromList(dstList);
|
||||||
|
|
||||||
|
print(
|
||||||
|
'✅ block‑токен успешно декомпрессирован: '
|
||||||
|
'$srcSize → ${decompressed.length} байт',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Пытаемся интерпретировать результат как msgpack — многие блоки
|
||||||
|
// содержат внутри ещё один msgpack‑объект.
|
||||||
|
final nested = _deserializeMsgpack(decompressed);
|
||||||
|
if (nested != null) {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это не msgpack — вернём просто байты, вызывающий код сам решит,
|
||||||
|
// что с ними делать.
|
||||||
|
return decompressed;
|
||||||
|
} finally {
|
||||||
|
malloc.free(srcPtr);
|
||||||
|
malloc.free(dstPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFI недоступен — пробуем наш чистый Dart‑декодер LZ4 block.
|
||||||
|
try {
|
||||||
|
final decompressed =
|
||||||
|
_lz4DecompressBlockPure(compressedBytes, 500000 /* max */);
|
||||||
|
print(
|
||||||
|
'✅ block‑токен декомпрессирован через чистый LZ4 block: '
|
||||||
|
'${compressedBytes.length} → ${decompressed.length} байт',
|
||||||
|
);
|
||||||
|
|
||||||
|
final nested = _deserializeMsgpack(decompressed);
|
||||||
|
return nested ?? decompressed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Ошибка десериализации msgpack: $e');
|
print('⚠️ Не удалось декомпрессировать block‑токен через чистый LZ4: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Ошибка при разборе block‑токена: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,194 +517,23 @@ class RegistrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Сначала пробуем LZ4 декомпрессию как в register.py
|
// Сначала пробуем LZ4 block‑декомпрессию так же, как делает register.py
|
||||||
|
// (lz4.block.decompress(payload_bytes, uncompressed_size=99999)).
|
||||||
Uint8List decompressedBytes = payloadBytes;
|
Uint8List decompressedBytes = payloadBytes;
|
||||||
|
|
||||||
// Если данные сжаты (compFlag != 0), пробуем LZ4 block декомпрессию
|
|
||||||
if (isCompressed && payloadBytes.length > 4) {
|
|
||||||
print('📦 Данные помечены как сжатые (compFlag != 0)');
|
|
||||||
|
|
||||||
// Пробуем LZ4 block декомпрессию через FFI (как в register.py)
|
|
||||||
try {
|
try {
|
||||||
if (_lz4BlockDecompress != null) {
|
print('📦 Пробуем LZ4 block‑декомпрессию (чистый Dart)...');
|
||||||
print('📦 Попытка LZ4 block декомпрессии через FFI...');
|
decompressedBytes = _lz4DecompressBlockPure(payloadBytes, 500000);
|
||||||
|
|
||||||
// В register.py используется фиксированный uncompressed_size=99999
|
|
||||||
// И данные используются полностью (без пропуска первых 4 байт)
|
|
||||||
// Но в packet_framer.dart при compFlag пропускаются первые 4 байта
|
|
||||||
// Попробуем оба варианта
|
|
||||||
|
|
||||||
// Вариант 1: как в register.py - используем все данные с фиксированным размером
|
|
||||||
// Увеличиваем размер для больших ответов (как в register.py используется 99999, но может быть недостаточно)
|
|
||||||
int uncompressedSize =
|
|
||||||
500000; // Увеличенный размер для больших ответов
|
|
||||||
Uint8List compressedData = payloadBytes;
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'📦 Попытка 1: Используем все данные с uncompressed_size=99999 (как в register.py)',
|
'✅ LZ4 block‑декомпрессия успешна: '
|
||||||
);
|
'${payloadBytes.length} → ${decompressedBytes.length} байт',
|
||||||
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) {
|
} catch (lz4Error) {
|
||||||
// Если LZ4 не удалась (данные не сжаты), используем сырые данные
|
// Как и в Python‑скрипте: если lz4 не сработал, просто используем сырые байты.
|
||||||
print(
|
print('⚠️ LZ4 block‑декомпрессия не применена: $lz4Error');
|
||||||
'⚠️ LZ4 декомпрессия не применена (данные не сжаты): $lz4Error',
|
print('📦 Используем сырые данные без распаковки...');
|
||||||
);
|
|
||||||
decompressedBytes = payloadBytes;
|
decompressedBytes = payloadBytes;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return _deserializeMsgpack(decompressedBytes);
|
return _deserializeMsgpack(decompressedBytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -539,6 +543,90 @@ class RegistrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Простейшая реализация LZ4 block‑декомпрессии на Dart.
|
||||||
|
/// Поддерживает стандартный формат блоков без фрейм‑заголовка.
|
||||||
|
/// Используется как аналог lz4.block.decompress из Python‑скрипта.
|
||||||
|
Uint8List _lz4DecompressBlockPure(Uint8List src, int maxOutputSize) {
|
||||||
|
// Алгоритм основан на официальной спецификации LZ4.
|
||||||
|
final dst = BytesBuilder(copy: false);
|
||||||
|
int srcPos = 0;
|
||||||
|
|
||||||
|
while (srcPos < src.length) {
|
||||||
|
if (srcPos >= src.length) break;
|
||||||
|
final token = src[srcPos++];
|
||||||
|
var literalLen = token >> 4;
|
||||||
|
|
||||||
|
// Дополнительная длина литералов
|
||||||
|
if (literalLen == 15) {
|
||||||
|
while (srcPos < src.length) {
|
||||||
|
final b = src[srcPos++];
|
||||||
|
literalLen += b;
|
||||||
|
if (b != 255) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем литералы
|
||||||
|
if (literalLen > 0) {
|
||||||
|
if (srcPos + literalLen > src.length) {
|
||||||
|
throw StateError('LZ4: literal length выходит за пределы входного буфера');
|
||||||
|
}
|
||||||
|
final literals = src.sublist(srcPos, srcPos + literalLen);
|
||||||
|
srcPos += literalLen;
|
||||||
|
dst.add(literals);
|
||||||
|
if (dst.length > maxOutputSize) {
|
||||||
|
throw StateError('LZ4: превышен максимально допустимый размер вывода');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конец блока — нет места даже на offset
|
||||||
|
if (srcPos >= src.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем offset
|
||||||
|
if (srcPos + 1 >= src.length) {
|
||||||
|
throw StateError('LZ4: неполный offset в потоке');
|
||||||
|
}
|
||||||
|
final offset = src[srcPos] | (src[srcPos + 1] << 8);
|
||||||
|
srcPos += 2;
|
||||||
|
|
||||||
|
if (offset == 0) {
|
||||||
|
throw StateError('LZ4: offset не может быть 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchLen = (token & 0x0F) + 4;
|
||||||
|
|
||||||
|
// Дополнительная длина match‑а
|
||||||
|
if ((token & 0x0F) == 0x0F) {
|
||||||
|
while (srcPos < src.length) {
|
||||||
|
final b = src[srcPos++];
|
||||||
|
matchLen += b;
|
||||||
|
if (b != 255) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем match из уже записанных данных
|
||||||
|
final dstBytes = dst.toBytes();
|
||||||
|
final dstLen = dstBytes.length;
|
||||||
|
final matchPos = dstLen - offset;
|
||||||
|
if (matchPos < 0) {
|
||||||
|
throw StateError('LZ4: match указывает за пределы уже декодированных данных');
|
||||||
|
}
|
||||||
|
|
||||||
|
final match = <int>[];
|
||||||
|
for (int i = 0; i < matchLen; i++) {
|
||||||
|
match.add(dstBytes[matchPos + (i % offset)]);
|
||||||
|
}
|
||||||
|
dst.add(Uint8List.fromList(match));
|
||||||
|
|
||||||
|
if (dst.length > maxOutputSize) {
|
||||||
|
throw StateError('LZ4: превышен максимально допустимый размер вывода');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uint8List.fromList(dst.toBytes());
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> _sendMessage(int opcode, Map<String, dynamic> payload) async {
|
Future<dynamic> _sendMessage(int opcode, Map<String, dynamic> payload) async {
|
||||||
if (!_isConnected || _socket == null) {
|
if (!_isConnected || _socket == null) {
|
||||||
throw Exception('Не подключено к серверу');
|
throw Exception('Не подключено к серверу');
|
||||||
|
|||||||
@@ -190,6 +190,69 @@ extension ApiServiceChats on ApiService {
|
|||||||
print('Переименовываем группу $chatId в: $newName');
|
print('Переименовываем группу $chatId в: $newName');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Создает/перегенерирует пригласительную ссылку для группы.
|
||||||
|
/// Сервер ожидает payload вида:
|
||||||
|
/// {"chatId": -69330645868731, "revokePrivateLink": true}
|
||||||
|
/// В ответ приходит объект с обновленным chat, где есть поле "link".
|
||||||
|
Future<String?> createGroupInviteLink(
|
||||||
|
int chatId, {
|
||||||
|
bool revokePrivateLink = true,
|
||||||
|
}) async {
|
||||||
|
final payload = {
|
||||||
|
"chatId": chatId,
|
||||||
|
"revokePrivateLink": revokePrivateLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
print('Создаем пригласительную ссылку для группы $chatId: $payload');
|
||||||
|
|
||||||
|
final int seq = _sendMessage(55, payload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await messages
|
||||||
|
.firstWhere((msg) => msg['seq'] == seq)
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
|
|
||||||
|
if (response['cmd'] == 3) {
|
||||||
|
final error = response['payload'];
|
||||||
|
print('Ошибка создания пригласительной ссылки: $error');
|
||||||
|
final message =
|
||||||
|
error?['localizedMessage'] ?? error?['message'] ?? 'Неизвестная ошибка';
|
||||||
|
throw Exception(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
final chat = response['payload']?['chat'];
|
||||||
|
final link = chat?['link'] as String?;
|
||||||
|
if (link == null || link.isEmpty) {
|
||||||
|
print(
|
||||||
|
'Пригласительная ссылка не найдена в ответе: ${response['payload']}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновим кэш чатов, если сервер вернул полный объект чата
|
||||||
|
try {
|
||||||
|
if (chat != null) {
|
||||||
|
final chats = _lastChatsPayload?['chats'] as List<dynamic>?;
|
||||||
|
if (chats != null) {
|
||||||
|
final index = chats.indexWhere(
|
||||||
|
(c) => c is Map && c['id'] == chat['id'],
|
||||||
|
);
|
||||||
|
if (index >= 0) {
|
||||||
|
chats[index] = chat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Не удалось обновить кэш чатов после создания ссылки: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return link;
|
||||||
|
} catch (e) {
|
||||||
|
print('Ошибка при создании пригласительной ссылки: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addGroupMember(
|
void addGroupMember(
|
||||||
int chatId,
|
int chatId,
|
||||||
List<int> userIds, {
|
List<int> userIds, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/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/contact.dart';
|
||||||
import 'package:gwid/services/avatar_cache_service.dart';
|
import 'package:gwid/services/avatar_cache_service.dart';
|
||||||
import 'package:gwid/widgets/user_profile_panel.dart';
|
import 'package:gwid/widgets/user_profile_panel.dart';
|
||||||
@@ -531,6 +532,45 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _createInviteLink() async {
|
||||||
|
try {
|
||||||
|
final link = await ApiService.instance.createGroupInviteLink(
|
||||||
|
widget.chatId,
|
||||||
|
revokePrivateLink: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (link == null || link.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Не удалось получить пригласительную ссылку'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clipboard.setData(ClipboardData(text: link));
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ссылка скопирована: $link'),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка при создании ссылки: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -553,6 +593,45 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _createInviteLink() async {
|
||||||
|
try {
|
||||||
|
final link = await ApiService.instance.createGroupInviteLink(
|
||||||
|
widget.chatId,
|
||||||
|
revokePrivateLink: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (link == null || link.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Не удалось получить пригласительную ссылку'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clipboard.setData(ClipboardData(text: link));
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ссылка скопирована: $link'),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка при создании ссылки: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -736,6 +815,18 @@ class _GroupSettingsScreenState extends State<GroupSettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _createInviteLink,
|
||||||
|
icon: const Icon(Icons.link),
|
||||||
|
label: const Text('Создать пригласительную ссылку'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
224
register.py
Normal file
224
register.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
import lz4.block
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
|
||||||
|
class MiniSocketClient:
|
||||||
|
def __init__(self, host, port, ssl_context=None, ping_interval=30):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.ssl_context = ssl_context or ssl.create_default_context()
|
||||||
|
self._socket = None
|
||||||
|
self._seq = 0
|
||||||
|
self._pending = {}
|
||||||
|
self.is_connected = False
|
||||||
|
self.ping_interval = ping_interval
|
||||||
|
self._recv_task = None
|
||||||
|
self._ping_task = None
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
raw_sock = await loop.run_in_executor(
|
||||||
|
None, lambda: socket.create_connection((self.host, self.port))
|
||||||
|
)
|
||||||
|
self._socket = self.ssl_context.wrap_socket(raw_sock, server_hostname=self.host)
|
||||||
|
self.is_connected = True
|
||||||
|
self._recv_task = asyncio.create_task(self._recv_loop())
|
||||||
|
self._ping_task = asyncio.create_task(self._ping_loop())
|
||||||
|
|
||||||
|
def _pack_packet(self, ver, cmd, seq, opcode, payload):
|
||||||
|
ver_b = ver.to_bytes(1, "big")
|
||||||
|
cmd_b = cmd.to_bytes(2, "big")
|
||||||
|
seq_b = seq.to_bytes(1, "big")
|
||||||
|
opcode_b = opcode.to_bytes(2, "big")
|
||||||
|
payload_bytes = msgpack.packb(payload)
|
||||||
|
payload_len = len(payload_bytes) & 0xFFFFFF
|
||||||
|
payload_len_b = payload_len.to_bytes(4, "big")
|
||||||
|
return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
|
||||||
|
|
||||||
|
def _unpack_packet(self, data):
|
||||||
|
payload_len = int.from_bytes(data[6:10], "big") & 0xFFFFFF
|
||||||
|
payload_bytes = data[10 : 10 + payload_len]
|
||||||
|
if payload_bytes:
|
||||||
|
try:
|
||||||
|
payload_bytes = lz4.block.decompress(
|
||||||
|
payload_bytes, uncompressed_size=99999
|
||||||
|
)
|
||||||
|
except lz4.block.LZ4BlockError:
|
||||||
|
pass
|
||||||
|
payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
|
||||||
|
else:
|
||||||
|
payload = None
|
||||||
|
return payload
|
||||||
|
|
||||||
|
async def send_msg(self, opcode: int, payload: dict):
|
||||||
|
if not self.is_connected:
|
||||||
|
raise RuntimeError("Socket not connected")
|
||||||
|
self._seq += 1
|
||||||
|
seq = self._seq
|
||||||
|
packet = self._pack_packet(
|
||||||
|
ver=10, cmd=0, seq=seq, opcode=opcode, payload=payload
|
||||||
|
)
|
||||||
|
fut = asyncio.get_running_loop().create_future()
|
||||||
|
self._pending[seq] = fut
|
||||||
|
await asyncio.get_running_loop().run_in_executor(
|
||||||
|
None, lambda: self._socket.sendall(packet)
|
||||||
|
)
|
||||||
|
return await fut
|
||||||
|
|
||||||
|
async def _recv_loop(self):
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
def _recv_exactly(n):
|
||||||
|
buf = bytearray()
|
||||||
|
while len(buf) < n:
|
||||||
|
chunk = self._socket.recv(n - len(buf))
|
||||||
|
if not chunk:
|
||||||
|
return bytes(buf)
|
||||||
|
buf.extend(chunk)
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
|
while self.is_connected:
|
||||||
|
try:
|
||||||
|
header = await loop.run_in_executor(None, lambda: _recv_exactly(10))
|
||||||
|
if not header:
|
||||||
|
self.is_connected = False
|
||||||
|
break
|
||||||
|
payload_len = int.from_bytes(header[6:10], "big") & 0xFFFFFF
|
||||||
|
payload_bytes = await loop.run_in_executor(
|
||||||
|
None, lambda: _recv_exactly(payload_len)
|
||||||
|
)
|
||||||
|
payload = self._unpack_packet(header + payload_bytes)
|
||||||
|
seq = int.from_bytes(header[3:4], "big")
|
||||||
|
fut = self._pending.pop(seq, None)
|
||||||
|
if fut and not fut.done():
|
||||||
|
fut.set_result(payload)
|
||||||
|
except Exception as e:
|
||||||
|
print("Recv loop error:", e)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def _ping_loop(self):
|
||||||
|
while self.is_connected:
|
||||||
|
try:
|
||||||
|
await self.send_msg(opcode=1, payload={})
|
||||||
|
except Exception as e:
|
||||||
|
print("Ping failed:", e)
|
||||||
|
await asyncio.sleep(self.ping_interval)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Gracefully stop background tasks and close the socket."""
|
||||||
|
self.is_connected = False
|
||||||
|
try:
|
||||||
|
if self._socket:
|
||||||
|
try:
|
||||||
|
self._socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self._socket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if self._recv_task:
|
||||||
|
try:
|
||||||
|
self._recv_task.cancel()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if self._ping_task:
|
||||||
|
try:
|
||||||
|
self._ping_task.cancel()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main(phone_number: str):
|
||||||
|
client = MiniSocketClient("api.oneme.ru", 443)
|
||||||
|
await client.connect()
|
||||||
|
h_json = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
response = await client.send_msg(opcode=6, payload=h_json)
|
||||||
|
print(json.dumps(response, indent=4, ensure_ascii=False))
|
||||||
|
err = response.get("payload", {}).get("error")
|
||||||
|
if err:
|
||||||
|
print("Error:", err)
|
||||||
|
|
||||||
|
sa_json = {"type": "START_AUTH", "phone": phone_number}
|
||||||
|
|
||||||
|
response = await client.send_msg(opcode=17, payload=sa_json)
|
||||||
|
print(json.dumps(response, indent=4, ensure_ascii=False))
|
||||||
|
err = response.get("payload", {}).get("error")
|
||||||
|
if err:
|
||||||
|
print("Error:", err)
|
||||||
|
|
||||||
|
# token may appear in payload or at the top level, handle both
|
||||||
|
payload = response.get("payload") or {}
|
||||||
|
temp_token = payload.get("token") or response.get("token")
|
||||||
|
if not temp_token:
|
||||||
|
print(
|
||||||
|
"No auth token returned in response",
|
||||||
|
json.dumps(response, indent=4, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
code = await asyncio.get_running_loop().run_in_executor(
|
||||||
|
None, input, "Enter verification code: "
|
||||||
|
)
|
||||||
|
sc_json = {
|
||||||
|
"verifyCode": code,
|
||||||
|
"token": temp_token,
|
||||||
|
"authTokenType": "CHECK_CODE",
|
||||||
|
}
|
||||||
|
response = await client.send_msg(opcode=18, payload=sc_json)
|
||||||
|
print(json.dumps(response, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
|
err = response.get("payload", {}).get("error")
|
||||||
|
if err:
|
||||||
|
print("Error:", err)
|
||||||
|
token_src = response.get("payload") or response
|
||||||
|
reg_token = token_src.get("tokenAttrs", {}).get("REGISTER", {}).get("token")
|
||||||
|
if not reg_token:
|
||||||
|
print(
|
||||||
|
"No register token returned in response",
|
||||||
|
json.dumps(response, indent=4, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
print("Registering with token:", reg_token)
|
||||||
|
rg_json = {
|
||||||
|
"lastName": "G",
|
||||||
|
"token": reg_token,
|
||||||
|
"firstName": "Kirill",
|
||||||
|
"tokenType": "REGISTER",
|
||||||
|
}
|
||||||
|
response = await client.send_msg(opcode=23, payload=rg_json)
|
||||||
|
err = response.get("payload", {}).get("error")
|
||||||
|
if err:
|
||||||
|
print("Error:", err)
|
||||||
|
print(json.dumps(response, indent=4, ensure_ascii=False))
|
||||||
|
print(response.get("payload", {}).get("token") or response.get("token"))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
print("Done, connection closed")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main("+79230556736"))
|
||||||
Reference in New Issue
Block a user