From d9cbadbbb7f52cae65bea8e4f0bb435bf760005f Mon Sep 17 00:00:00 2001 From: prime Date: Wed, 29 Apr 2026 19:02:44 +1000 Subject: [PATCH] =?UTF-8?q?=D1=80=D1=83=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=BC=D0=B0=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +++++- lib/backend/api.dart | 6 ++++ lib/backend/models/chat_folder.dart | 2 +- lib/backend/modules/account.dart | 28 +++++++++------- lib/backend/modules/calls.dart | 4 +-- lib/backend/modules/chats.dart | 32 +++++++++++++------ lib/backend/modules/contacts.dart | 12 +++---- lib/backend/modules/messages.dart | 19 ++++++----- lib/core/protocol/packet.dart | 1 + lib/core/storage/app_database.dart | 28 +++++++++++++--- lib/core/storage/token_storage.dart | 2 +- lib/core/transport/connection.dart | 4 ++- lib/core/transport/proxy_connector.dart | 24 ++++++++++---- lib/core/transport/sender.dart | 2 +- .../screens/auth/server_settings_sheet.dart | 4 ++- .../screens/chats/chat_list_screen.dart | 1 + lib/frontend/screens/chats/chat_screen.dart | 2 +- .../screens/profile/devices_screen.dart | 5 ++- lib/main.dart | 13 +++++--- lib/models/attachment.dart | 12 ++++--- 20 files changed, 145 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 14601b5..f395ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,12 @@ app.*.symbols !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock \ No newline at end of file +!/dev/ci/**/Gemfile.lock + +# AI / Agents +agents.md +.claude/ + +# Environment variables +.env +.env.* \ No newline at end of file diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 2c50950..495b56a 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -237,6 +237,11 @@ class Api { _dispatcher.registerHandler(opcode, handler); } + /// Снимает обработчик пушей с указанного опкода. + void unregisterPushHandler(int opcode) { + _dispatcher.unregisterHandler(opcode); + } + /// Стрим всех входящих пушей от сервера. Stream get pushStream => _dispatcher.pushStream; @@ -248,6 +253,7 @@ class Api { _connection.dispose(); _stateController.close(); _sessionExpiredController.close(); + _handshakeSuccessController.close(); } // Внутрянка diff --git a/lib/backend/models/chat_folder.dart b/lib/backend/models/chat_folder.dart index 0d6044a..8392a6c 100644 --- a/lib/backend/models/chat_folder.dart +++ b/lib/backend/models/chat_folder.dart @@ -25,7 +25,7 @@ class ChatFolder { factory ChatFolder.fromJson(Map json) { return ChatFolder( - id: json['id'].toString(), + id: json['id']?.toString() ?? '', title: json['title']?.toString() ?? '', emoji: json['emoji']?.toString(), include: (json['include'] as List?) diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 1f57233..4c5c32d 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -289,7 +289,7 @@ class LoginSyncParams { draftsSync: int.tryParse(values[SyncKey.draftsSync] ?? '') ?? 0, bannersSync: int.tryParse(values[SyncKey.bannersSync] ?? '') ?? 0, presenceSync: int.tryParse(values[SyncKey.presenceSync] ?? '') ?? -1, - lastLogin: int.parse(lastLogin), + lastLogin: int.tryParse(lastLogin) ?? 0, configHash: values[SyncKey.configHash], chatCacheFingerprint: values[SyncKey.chatCacheFingerprint], ); @@ -646,19 +646,25 @@ class AccountModule { Future _processProfileUpdate(Packet packet) async { _api.registerPushHandler(Opcode.notifProfile, (p) {}); - await for (final push in _api.pushStream.where( - (p) => p.opcode == Opcode.notifProfile, - )) { - final payload = push.payload; - if (payload is Map) { - final profile = payload['profile']; - if (profile is Map) { - final contact = profile['contact']; - if (contact is Map) { - return ProfileData.fromServerMap(contact.cast()); + try { + await for (final push in _api.pushStream + .where((p) => p.opcode == Opcode.notifProfile) + .timeout(const Duration(seconds: 15))) { + final payload = push.payload; + if (payload is Map) { + final profile = payload['profile']; + if (profile is Map) { + final contact = profile['contact']; + if (contact is Map) { + return ProfileData.fromServerMap(contact.cast()); + } } } } + } on TimeoutException { + throw Exception('Таймаут ожидания обновления профиля'); + } finally { + _api.unregisterPushHandler(Opcode.notifProfile); } throw Exception('Не удалось получить обновлённый профиль'); } diff --git a/lib/backend/modules/calls.dart b/lib/backend/modules/calls.dart index 4593e48..cc2db6a 100644 --- a/lib/backend/modules/calls.dart +++ b/lib/backend/modules/calls.dart @@ -92,8 +92,8 @@ class CallsModule { msg['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(); - final name = contact?.firstName != null - ? '${contact!.firstName} ${contact.lastName ?? ''}'.trim() + final name = (contact != null && contact.firstName.isNotEmpty) + ? '${contact.firstName} ${contact.lastName ?? ''}'.trim() : 'Неизвестный'; extractedCalls.add( diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index 3b0fb27..7ee2864 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -3,6 +3,21 @@ import 'dart:convert'; import '../../core/storage/app_database.dart'; import '../../core/utils/logger.dart'; +Map _parseParticipants(dynamic raw) { + try { + final decoded = raw is String ? jsonDecode(raw) : raw; + if (decoded is Map) { + return decoded.map((k, v) => MapEntry( + k is int ? k : int.parse(k.toString()), + v is int ? v : int.tryParse(v.toString()) ?? 0, + )); + } + } catch (e) { + logger.e('Failed to parse participants: $e'); + } + return {}; +} + class CachedChat { final int id; final int accountId; @@ -59,8 +74,7 @@ class CachedChat { dontDisturbUntil: row['dont_disturb_until'] as int, isOnline: (row['is_online'] as int) == 1, seenTime: row['seen_time'] as int, - // watafuc - participants: Map.from(jsonDecode(row['participants'])).map((k, v) => MapEntry(int.parse(k), v)) + participants: _parseParticipants(row['participants']) ); Map toDbRow() => { @@ -242,7 +256,7 @@ class ChatsModule { isOnline = (presence['status'] as int?) == 1; } } - Map participants = Map.from(chat['participants']); + Map participants = _parseParticipants(chat['participants']); return CachedChat( id: id, @@ -282,12 +296,12 @@ class ChatsModule { static String? _nameFromContact(Map contact) { final names = contact['names']; if (names is! List || names.isEmpty) return null; - final name = - names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.first, - ) - as Map; + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is! Map) return null; + final name = nameRaw; return name['name'] as String?; } } diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 1943e52..71e4926 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -72,12 +72,12 @@ class ContactsModule { final names = contact['names']; if (names is List && names.isNotEmpty) { - final name = - names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.first, - ) - as Map; + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is! Map) return null; + final name = nameRaw; firstName = (name['firstName'] as String?) ?? ''; lastName = name['lastName'] as String?; } diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 1526cac..255ff2e 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -155,7 +155,9 @@ class MessagesModule { } if (rows.isNotEmpty) { - AppDatabase.saveMessages(rows).ignore(); + AppDatabase.saveMessages(rows).catchError((e) { + debugPrint('saveMessages error: $e'); + }); } return results; @@ -257,9 +259,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; @@ -295,9 +296,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; @@ -333,9 +333,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 7b6fb38..5637328 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -190,6 +190,7 @@ Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) { 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'); diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 5890f21..53bbb91 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:komet/core/utils/logger.dart'; @@ -72,10 +73,15 @@ class ProfileData { final profileOptionsStr = row['profile_options'] as String?; List? profileOptions; if (profileOptionsStr != null && profileOptionsStr.isNotEmpty) { - profileOptions = profileOptionsStr - .split(',') - .map((e) => int.parse(e.trim())) - .toList(); + try { + profileOptions = profileOptionsStr + .split(',') + .where((e) => e.trim().isNotEmpty) + .map((e) => int.parse(e.trim())) + .toList(); + } catch (_) { + profileOptions = null; + } } return ProfileData( id: row['id'] as int, @@ -130,8 +136,20 @@ class AppDatabase { } } + static Completer? _initCompleter; + static Future get _instance async { - _db ??= await _open(); + if (_db != null) return _db!; + if (_initCompleter != null) return _initCompleter!.future; + _initCompleter = Completer(); + try { + _db = await _open(); + _initCompleter!.complete(_db!); + } catch (e) { + _initCompleter!.completeError(e); + _initCompleter = null; + rethrow; + } return _db!; } diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 75ab4bb..b04ed77 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -33,7 +33,7 @@ class TokenStorage { static Future readActiveToken() async { final id = await getActiveAccountId(); if (id == null) return null; - return readToken(id); + return await readToken(id); } static Future deleteAccount(int accountId) async { diff --git a/lib/core/transport/connection.dart b/lib/core/transport/connection.dart index 15c3a53..c0b0532 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -99,7 +99,9 @@ class Connection { if (socket != null) { try { socket.close(); - } catch (_) {} + } catch (e) { + logger.w('Ошибка при закрытии сокета: $e'); + } } _setState(SocketState.disconnected); diff --git a/lib/core/transport/proxy_connector.dart b/lib/core/transport/proxy_connector.dart index aaac29b..90d4df6 100644 --- a/lib/core/transport/proxy_connector.dart +++ b/lib/core/transport/proxy_connector.dart @@ -60,8 +60,8 @@ class ProxyConnector { if (!useAuth) { throw SocketException('SOCKS5: прокси требует аутентификацию'); } - final usernameBytes = utf8.encode(settings.username!); - final passwordBytes = utf8.encode(settings.password!); + final usernameBytes = utf8.encode(settings.username ?? ''); + final passwordBytes = utf8.encode(settings.password ?? ''); final authPacket = BytesBuilder() ..addByte(0x01) ..addByte(usernameBytes.length) @@ -175,7 +175,10 @@ class ProxyConnector { final responseStr = utf8.decode(headerBytes, allowMalformed: true); final statusLine = responseStr.split('\r\n').first; final parts = statusLine.split(' '); - final statusCode = parts.length >= 2 ? int.tryParse(parts[1]) ?? 0 : 0; + if (parts.length < 2) { + throw SocketException('HTTP CONNECT: некорректный ответ: $statusLine'); + } + final statusCode = int.tryParse(parts[1]) ?? 0; if (statusCode != 200) { throw SocketException( 'HTTP CONNECT: прокси вернул статус $statusCode', @@ -201,10 +204,17 @@ class ProxyConnector { RawSocket proxySocket, _RawSocketIO io, ) async { - final server = await RawServerSocket.bind( - InternetAddress.loopbackIPv4, - 0, - ); + RawServerSocket? server; + try { + server = await RawServerSocket.bind( + InternetAddress.loopbackIPv4, + 0, + ); + } catch (e) { + io.dispose(); + proxySocket.close(); + rethrow; + } final clientSide = await RawSocket.connect( InternetAddress.loopbackIPv4, server.port, diff --git a/lib/core/transport/sender.dart b/lib/core/transport/sender.dart index 69e8c5d..aaa3187 100644 --- a/lib/core/transport/sender.dart +++ b/lib/core/transport/sender.dart @@ -8,7 +8,7 @@ class PacketSender { int get currentSeq => _seq; int _nextSeq() { - _seq = (_seq + 1) % 256; + _seq = (_seq + 1) % 65536; return _seq; } diff --git a/lib/frontend/screens/auth/server_settings_sheet.dart b/lib/frontend/screens/auth/server_settings_sheet.dart index 534338a..e458642 100644 --- a/lib/frontend/screens/auth/server_settings_sheet.dart +++ b/lib/frontend/screens/auth/server_settings_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -53,7 +55,7 @@ class _ServerSettingsSheetState extends State { await prefs.setString(ServerConfig.prefHostKey, host); await prefs.setInt(ServerConfig.prefPortKey, port); await api.disconnect(); - api.connect(); + unawaited(api.connect()); final online = await api.stateStream .firstWhere((s) => s == SessionState.online || s == SessionState.disconnected) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 9a9878b..be2dacf 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -638,6 +638,7 @@ class _ChatListScreenState extends State ..removeListener(_onStoriesRevealTick) ..removeStatusListener(_onStoriesRevealStatus) ..dispose(); + _shimmerController.dispose(); _folderPageController.dispose(); while (_folderChatScrollControllers.isNotEmpty) { final c = _folderChatScrollControllers.removeLast(); diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 41069ac..22c8d0c 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -381,7 +381,7 @@ class _ChatScreenState extends State myId: _myId, prevMessage: prevMessage, nextMessage: nextMessage, - chatType: chat!.type, + chatType: chat?.type ?? 'CHAT', ); }, ); diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart index 8030921..ab9b715 100644 --- a/lib/frontend/screens/profile/devices_screen.dart +++ b/lib/frontend/screens/profile/devices_screen.dart @@ -271,8 +271,9 @@ class _DevicesScreenState extends State setState(() => _loadingIps.add(id)); } + HttpClient? client; try { - final client = HttpClient(); + client = HttpClient(); client.connectionTimeout = const Duration(seconds: 5); final request = await client.getUrl( Uri.parse( @@ -296,6 +297,8 @@ class _DevicesScreenState extends State setState(() => _loadingIps.remove(id)); showCustomNotification(context, 'Ошибка IP: $e'); } + } finally { + client?.close(); } } diff --git a/lib/main.dart b/lib/main.dart index 0ce4998..5c62627 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -71,6 +73,7 @@ class KometAppState extends State { late Locale _locale; bool _isLoggingOut = false; + StreamSubscription? _sessionExpiredSub; late final ValueNotifier fpsOverlayEnabled = ValueNotifier( widget.initialFpsOverlay, ); @@ -83,15 +86,16 @@ class KometAppState extends State { api.setReconnectCallback(() async { try { final accountId = await TokenStorage.getActiveAccountId(); - if (accountId != null && - await TokenStorage.readToken(accountId) != null) { + if (accountId != null) { final token = await TokenStorage.readToken(accountId); - await accountModule.login(accountId: accountId, token: token); + if (token != null) { + await accountModule.login(accountId: accountId, token: token); + } } } catch (_) {} }); - api.sessionExpiredStream.listen((SessionExpiredException e) async { + _sessionExpiredSub = api.sessionExpiredStream.listen((SessionExpiredException e) async { if (_isLoggingOut) return; _isLoggingOut = true; @@ -118,6 +122,7 @@ class KometAppState extends State { @override void dispose() { + _sessionExpiredSub?.cancel(); fpsOverlayEnabled.dispose(); super.dispose(); } diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 25ce0a6..825e348 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -136,7 +136,8 @@ class VideoAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -192,7 +193,8 @@ class AudioAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -244,7 +246,8 @@ class FileAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -294,7 +297,8 @@ class StickerAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} }