починил рег, кто так смержил уебищно скажите мне???? Добавил кнопку создание приглосительной ссылки в чат. Больше пока ничего не добавляем - жду пока дед запушит и займусь багофиксом, если дед все не закроет

This commit is contained in:
jganenok
2025-12-01 17:38:51 +07:00
parent c278f2d421
commit 7e0d5eba20
4 changed files with 659 additions and 193 deletions

View File

@@ -2,7 +2,6 @@ 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;
@@ -29,7 +28,9 @@ class RegistrationService {
bool _isConnected = false;
Timer? _pingTimer;
StreamSubscription? _socketSubscription;
Lz4Codec? _lz4Codec;
// LZ4 через es_compression/FFI сейчас не работает на Windows из‑за отсутствия
// eslz4-win64.dll, поэтому ниже реализован свой чистый декодер LZ4 block.
// Поля для LZ4 через FFI оставлены на будущее, если появится корректная DLL.
DynamicLibrary? _lz4Lib;
Lz4Decompress? _lz4BlockDecompress;
@@ -315,19 +316,193 @@ class RegistrationService {
dynamic _deserializeMsgpack(Uint8List data) {
print('📦 Десериализация msgpack...');
try {
final payload = msgpack.deserialize(data);
dynamic payload = msgpack.deserialize(data);
print('✅ Msgpack десериализация успешна');
// Проверяем, что получили валидный результат (не просто число)
if (payload is int && payload < 0) {
// Иногда сервер шлёт FFIтокены в виде "отрицательное число + настоящий объект"
// в одном 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(
'⚠️ Получено отрицательное число вместо 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 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) {
print('❌ Ошибка десериализации msgpack: $e');
print('⚠️ Не удалось декомпрессировать blockтокен через чистый LZ4: $e');
return null;
}
} catch (e) {
print('⚠️ Ошибка при разборе blockтокена: $e');
return null;
}
}
@@ -342,194 +517,23 @@ class RegistrationService {
}
try {
// Сначала пробуем LZ4 декомпрессию как в register.py
// Сначала пробуем LZ4 blockдекомпрессию так же, как делает register.py
// (lz4.block.decompress(payload_bytes, uncompressed_size=99999)).
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('📦 Пробуем LZ4 blockдекомпрессию (чистый Dart)...');
decompressedBytes = _lz4DecompressBlockPure(payloadBytes, 500000);
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} байт',
'✅ LZ4 blockдекомпрессия успешна: '
'${payloadBytes.length}${decompressedBytes.length} байт',
);
} catch (lz4Error) {
// Если LZ4 не удалась (данные не сжаты), используем сырые данные
print(
'⚠️ LZ4 декомпрессия не применена (данные не сжаты): $lz4Error',
);
// Как и в Pythonскрипте: если lz4 не сработал, просто используем сырые байты.
print('⚠️ LZ4 blockдекомпрессия не применена: $lz4Error');
print('📦 Используем сырые данные без распаковки...');
decompressedBytes = payloadBytes;
}
}
return _deserializeMsgpack(decompressedBytes);
} 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 {
if (!_isConnected || _socket == null) {
throw Exception('Не подключено к серверу');

View File

@@ -190,6 +190,69 @@ extension ApiServiceChats on ApiService {
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(
int chatId,
List<int> userIds, {

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/api/api_service.dart';
import 'package:flutter/services.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/services/avatar_cache_service.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) {
if (mounted) {
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
Widget build(BuildContext context) {
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 Divider(),
const SizedBox(height: 8),

224
register.py Normal file
View 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"))