diff --git a/lib/backend/modules/file_uploader.dart b/lib/backend/modules/file_uploader.dart index 5d5be94..3967ebe 100644 --- a/lib/backend/modules/file_uploader.dart +++ b/lib/backend/modules/file_uploader.dart @@ -101,6 +101,7 @@ class FileUploader { url: info.url, path: file.path, filename: filename, + connection: 'close', ) .listen( (e) { @@ -185,6 +186,8 @@ class FileUploader { url: uri.toString(), path: file.path, filename: _syntheticFilename(), + contentType: 'application/octet-stream', + connection: 'close', ), onProgress: onProgress, ); @@ -277,6 +280,11 @@ class FileUploader { ), onProgress: onProgress, ); + if (result.error != null || result.status != 200) { + logger.w( + 'uploadVideoFile: status=${result.status} error=${result.error}', + ); + } return result.error == null && result.status == 200; } catch (e) { logger.w('uploadVideoFile: $e'); diff --git a/lib/core/calls/conversation_params.dart b/lib/core/calls/conversation_params.dart index 64a9e2a..8000c77 100644 --- a/lib/core/calls/conversation_params.dart +++ b/lib/core/calls/conversation_params.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import '../protocol/lz4_block.dart'; +import 'package:kolibri/kolibri.dart' as kb; /// Параметры подключения к звонку (`vcp`), которые сервер присылает в пуше /// входящего звонка (opcode 137) и в ответе на инициацию исходящего. @@ -79,73 +76,19 @@ class ConversationParams { return nowSec >= expiresAt! - 5; } - static List _splitTurn(Object? value) { - if (value is! String || value.isEmpty) return const []; - return value - .split(',') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .toList(); - } - - static List _stringList(Object? value) { - if (value is! List) return const []; - return value.whereType().toList(); - } - - /// Распаковывает и парсит строку `vcp`. Возвращает `null`, если формат - /// не распознан. + /// Распаковывает и парсит строку `vcp` через Rust-ядро (kolibri). Возвращает + /// `null`, если формат не распознан. Требует инициализации `initKolibri()`. static ConversationParams? decode(String vcp) { - final sep = vcp.indexOf(':'); - if (sep <= 0) return null; - - final rawLen = int.tryParse(vcp.substring(0, sep)); - if (rawLen == null || rawLen <= 0) return null; - - final Uint8List compressed; - try { - compressed = base64.decode(vcp.substring(sep + 1)); - } catch (_) { - return null; - } - - final Uint8List bytes; - try { - final decompressed = lz4BlockDecompress(compressed, rawLen); - bytes = decompressed.length > rawLen - ? Uint8List.sublistView(decompressed, 0, rawLen) - : decompressed; - } catch (_) { - return null; - } - - final Object? json; - try { - json = jsonDecode(utf8.decode(bytes)); - } catch (_) { - return null; - } - if (json is! Map) return null; - - final token = json['tkn']; - final wse = json['wse']; - if (token is! String || wse is! String) return null; - + final kb.CallParams? p = kb.decodeVcp(vcp: vcp, conversationId: ''); + if (p == null) return null; return ConversationParams( - token: token, - wsEndpoint: wse, - wsIps: _stringList(json['wsip']), - wtEndpoint: json['wte'] as String?, - wtIps: _stringList(json['wtip']), - callsApiEndpoint: json['vcae'] as String?, - callsApiIps: _stringList(json['vcaip']), - clientType: json['srcp'] as String?, - expiresAt: json['et'] is int ? json['et'] as int : null, - stun: json['stne'] as String?, - turn: _splitTurn(json['trne']), - turnUser: json['trnu'] as String?, - turnPassword: json['trnp'] as String?, - isVideo: json['iv'] == true, + token: p.token, + wsEndpoint: p.wsEndpoint, + stun: p.stun, + turn: p.turn, + turnUser: p.turnUser, + turnPassword: p.turnPassword, + isVideo: p.isVideo, ); } } diff --git a/lib/core/calls/ws2_signaling.dart b/lib/core/calls/ws2_signaling.dart index 5f0f242..d3d952b 100644 --- a/lib/core/calls/ws2_signaling.dart +++ b/lib/core/calls/ws2_signaling.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; -import '../utils/logger.dart'; +import 'package:kolibri/kolibri.dart' as kb; + import 'conversation_params.dart'; /// Параметры подключения к сигналинг-сокету ws2. @@ -84,17 +84,19 @@ class Ws2CommandException implements Exception { /// Клиент сигналинга звонка поверх WebSocket `ws2`. /// -/// Конверт сообщений (подтверждено захватом `docs/ws2_capture.log`): +/// Тонкий адаптер над Rust-ядром (kolibri [kb.CallSignaling]): ядро держит +/// WebSocket, корреляцию `sequence`/`response`, keepalive `ping`→`pong` и +/// разбор кадров; здесь — прежний Dart-интерфейс для [call_session]. +/// +/// Конверт сообщений: /// - запрос: `{"command": ..., ..., "sequence": N}` /// - ответ: `{"sequence": N, "response": "", "type": "response"}` /// - пуш: `{..., "notification": "", "type": "notification"}` -/// - keepalive: текстовый кадр `ping` → ответ `pong`. class Ws2Signaling { final Ws2Config config; - WebSocket? _socket; - int _sequence = 0; - final Map>> _pending = {}; + kb.CallSignaling? _call; + StreamSubscription? _notifSub; final _notifications = StreamController>.broadcast(); final _closed = Completer(); @@ -104,106 +106,60 @@ class Ws2Signaling { /// Пуши сервера (`type == "notification"`). Фильтруй по полю `notification`. Stream> get notifications => _notifications.stream; - /// Завершается, когда сокет закрыт (значение — причина закрытия, если была). + /// Завершается, когда сокет закрыт. Future get done => _closed.future; - bool get isConnected => _socket != null; + bool get isConnected => _call?.isConnected() ?? false; Future connect() async { - final socket = await WebSocket.connect( - config.uri.toString(), - headers: {'User-Agent': 'okhttp/4.12.0'}, + final call = await kb.connectCallSignaling( + url: config.uri.toString(), + userAgent: 'okhttp/4.12.0', ); - _socket = socket; - socket.listen( - _onFrame, - onError: _onDone, + _call = call; + _notifSub = call.notifications().listen( + (json) { + Object? decoded; + try { + decoded = jsonDecode(json); + } catch (_) { + return; + } + if (decoded is Map) _notifications.add(decoded); + }, + onError: (_) => _onDone(null), onDone: () => _onDone(null), cancelOnError: false, ); } - void _onFrame(dynamic frame) { - if (frame is String && frame == 'ping') { - _socket?.add('pong'); - return; - } - - final String text; - if (frame is String) { - text = frame; - } else if (frame is List) { - text = utf8.decode(frame); - } else { - return; - } - - Object? decoded; - try { - decoded = jsonDecode(text); - } catch (_) { - return; - } - if (decoded is! Map) return; - - final label = - decoded['notification'] ?? decoded['response'] ?? decoded['type']; - final dump = jsonEncode(decoded); - logger.t('[ws2] ← $label'); - logger.t(dump.length > 1500 - ? '${dump.substring(0, 1500)}… (${dump.length}b)' - : dump); - - final type = decoded['type']; - if (type == 'response' || type == 'error') { - final seq = decoded['sequence']; - if (seq is int) { - final completer = _pending.remove(seq); - if (completer != null && !completer.isCompleted) { - completer.complete(decoded); - } - } - if (type == 'error') _notifications.add(decoded); - return; - } - - if (type == 'notification' || decoded.containsKey('notification')) { - _notifications.add(decoded); - } - } - void _onDone(Object? error) { - for (final c in _pending.values) { - if (!c.isCompleted) c.completeError(error ?? const SocketException('ws2 closed')); - } - _pending.clear(); if (!_closed.isCompleted) _closed.complete(error); if (!_notifications.isClosed) _notifications.close(); } /// Отправляет команду и ждёт ответ сервера. Бросает [Ws2CommandException], - /// если в ответе есть поле `error`. + /// если сервер вернул ошибку. Future> sendCommand( String command, { Map extra = const {}, Duration timeout = const Duration(seconds: 15), - }) { - final socket = _socket; - if (socket == null) { + }) async { + final call = _call; + if (call == null) { return Future.error(StateError('ws2 не подключён')); } - - final seq = ++_sequence; - final completer = Completer>(); - _pending[seq] = completer; - - socket.add(jsonEncode({'command': command, ...extra, 'sequence': seq})); - - return completer.future.timeout(timeout).then((response) { - final error = response['error']; - if (error != null) throw Ws2CommandException(command, error); - return response; - }); + try { + final response = await call + .sendCommand(command: command, extraJson: jsonEncode(extra)) + .timeout(timeout); + final decoded = jsonDecode(response); + return decoded is Map + ? decoded + : {}; + } catch (e) { + throw Ws2CommandException(command, e); + } } /// Передаёт SDP (offer/answer) другому участнику. @@ -329,7 +285,9 @@ class Ws2Signaling { extra: {'mediaSource': mediaSource, 'layers': layers}); Future close() async { - await _socket?.close(); - _socket = null; + await _notifSub?.cancel(); + _notifSub = null; + _call?.close(); + _call = null; } } diff --git a/lib/core/protocol/lz4_block.dart b/lib/core/protocol/lz4_block.dart deleted file mode 100644 index 55d1529..0000000 --- a/lib/core/protocol/lz4_block.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:typed_data'; - -/// LZ4 block декомпрессия (без frame-заголовка). -/// -/// Сервер шлёт block-формат как в транспорте (payload пакетов), так и в -/// `vcp`-параметрах звонка. dart_lz4 поддерживает только frame-формат, поэтому -/// block распаковывается вручную. -Uint8List lz4BlockDecompress(Uint8List src, int maxSize) { - var out = Uint8List(1024); - int outLen = 0; - int pos = 0; - - void ensure(int extra) { - if (outLen + extra > maxSize) throw StateError('LZ4: превышен лимит'); - if (outLen + extra <= out.length) return; - var newCap = out.length * 2; - while (newCap < outLen + extra) { - newCap *= 2; - } - if (newCap > maxSize) newCap = maxSize; - final grown = Uint8List(newCap); - grown.setRange(0, outLen, out); - out = grown; - } - - while (pos < src.length) { - final token = src[pos++]; - var litLen = token >> 4; - - if (litLen == 15) { - while (pos < src.length) { - final b = src[pos++]; - litLen += b; - if (b != 255) break; - } - } - - if (litLen > 0) { - ensure(litLen); - out.setRange(outLen, outLen + litLen, src, pos); - outLen += litLen; - pos += litLen; - } - - if (pos >= src.length) break; - - if (pos + 1 >= src.length) throw StateError('LZ4: unexpected end of input'); - final offset = src[pos] | (src[pos + 1] << 8); - pos += 2; - if (offset == 0) throw StateError('LZ4: offset = 0'); - - var matchLen = (token & 0x0F) + 4; - if ((token & 0x0F) == 0x0F) { - while (pos < src.length) { - final b = src[pos++]; - matchLen += b; - if (b != 255) break; - } - } - - ensure(matchLen); - final start = outLen - offset; - if (start < 0) throw StateError('LZ4: offset за пределами вывода'); - for (var i = 0; i < matchLen; i++) { - out[outLen + i] = out[start + i]; - } - outLen += matchLen; - } - - return Uint8List.sublistView(out, 0, outLen); -} diff --git a/lib/core/push/push_service.dart b/lib/core/push/push_service.dart index 4ecb356..3a21860 100644 --- a/lib/core/push/push_service.dart +++ b/lib/core/push/push_service.dart @@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:kolibri/kolibri.dart' show initKolibri; import 'package:shared_preferences/shared_preferences.dart'; import '../../backend/api.dart'; @@ -53,6 +54,9 @@ Future _handleCallDecline(String payloadJson) async { } if (vcp.isEmpty || conversationId.isEmpty) return; + // Фоновый изолят: инициализируем ядро перед vcp-декодом/сигналингом. + await initKolibri(); + final params = ConversationParams.decode(vcp); if (params == null) return; @@ -83,6 +87,7 @@ Future _handleReply(String payloadJson, String text) async { if (account == 0 || chatId == 0) return; WidgetsFlutterBinding.ensureInitialized(); + await initKolibri(); if (AppInstance.isNamed) { try { SharedPreferences.setPrefix('flutter.${AppInstance.id}.'); diff --git a/lib/frontend/screens/webapp/web_app_screen.dart b/lib/frontend/screens/webapp/web_app_screen.dart index 114db51..1acfd16 100644 --- a/lib/frontend/screens/webapp/web_app_screen.dart +++ b/lib/frontend/screens/webapp/web_app_screen.dart @@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/webapp.dart'; import '../../../core/storage/spoofing_service.dart'; +import '../../../main.dart' show api; import '../../widgets/connection_status.dart'; import '../../widgets/error_view.dart'; import '../../widgets/webview_permission_prompt.dart'; @@ -63,7 +64,11 @@ class _WebAppScreenState extends State { _launch = null; }); try { - _userAgent = await SpoofingService.getWebViewUserAgent() ?? ''; + // Тот же UA, что уходит в sessionInit (из handshake-устройства ядра), + // чтобы веб-аппы видели нативный клиент; фолбэк — браузерный UA спуфа. + _userAgent = api.session?.userAgent() ?? + await SpoofingService.getWebViewUserAgent() ?? + ''; final launch = await widget.loader(); if (!mounted) return; setState(() => _launch = launch); diff --git a/pubspec.lock b/pubspec.lock index 71bb253..57a1c9b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - dart_lz4: - dependency: "direct main" - description: - name: dart_lz4 - sha256: e2e9c30fdf83a7a1e63bc6c4d90786cf795c38eefee40b1f4c35ea6480c91db9 - url: "https://pub.dev" - source: hosted - version: "1.2.0" dart_webrtc: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8146c77..30eb5d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,13 +35,13 @@ dependencies: intl: any # Rust networking core (kolibri) — FFI plugin, replaces the Dart transport. - # Vendored as a git submodule (third_party/kolibri). + # DEV: local path for fast iteration; re-pin to third_party/kolibri submodule + # (path: third_party/kolibri/kolibri-dart) before merge. kolibri: path: third_party/kolibri/kolibri-dart # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - dart_lz4: ^1.0.0 crypto: ^3.0.7 ffi: ^2.1.0 logger: ^2.6.2 diff --git a/third_party/kolibri b/third_party/kolibri index 949e70a..8f6836c 160000 --- a/third_party/kolibri +++ b/third_party/kolibri @@ -1 +1 @@ -Subproject commit 949e70a1a5e9f38d57360cbc9869d701cec71da9 +Subproject commit 8f6836cac2198027697ed0702044426e84d6f78b