руки переломаю

This commit is contained in:
prime
2026-04-29 19:02:44 +10:00
parent 5db5dfabf5
commit d9cbadbbb7
20 changed files with 145 additions and 66 deletions
+9 -1
View File
@@ -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
!/dev/ci/**/Gemfile.lock
# AI / Agents
agents.md
.claude/
# Environment variables
.env
.env.*
+6
View File
@@ -237,6 +237,11 @@ class Api {
_dispatcher.registerHandler(opcode, handler);
}
/// Снимает обработчик пушей с указанного опкода.
void unregisterPushHandler(int opcode) {
_dispatcher.unregisterHandler(opcode);
}
/// Стрим всех входящих пушей от сервера.
Stream<Packet> get pushStream => _dispatcher.pushStream;
@@ -248,6 +253,7 @@ class Api {
_connection.dispose();
_stateController.close();
_sessionExpiredController.close();
_handshakeSuccessController.close();
}
// Внутрянка
+1 -1
View File
@@ -25,7 +25,7 @@ class ChatFolder {
factory ChatFolder.fromJson(Map<String, dynamic> json) {
return ChatFolder(
id: json['id'].toString(),
id: json['id']?.toString() ?? '',
title: json['title']?.toString() ?? '',
emoji: json['emoji']?.toString(),
include: (json['include'] as List<dynamic>?)
+17 -11
View File
@@ -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<ProfileData> _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<dynamic, dynamic>());
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<dynamic, dynamic>());
}
}
}
}
} on TimeoutException {
throw Exception('Таймаут ожидания обновления профиля');
} finally {
_api.unregisterPushHandler(Opcode.notifProfile);
}
throw Exception('Не удалось получить обновлённый профиль');
}
+2 -2
View File
@@ -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(
+23 -9
View File
@@ -3,6 +3,21 @@ import 'dart:convert';
import '../../core/storage/app_database.dart';
import '../../core/utils/logger.dart';
Map<int, int> _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<String, int>.from(jsonDecode(row['participants'])).map((k, v) => MapEntry(int.parse(k), v))
participants: _parseParticipants(row['participants'])
);
Map<String, dynamic> toDbRow() => {
@@ -242,7 +256,7 @@ class ChatsModule {
isOnline = (presence['status'] as int?) == 1;
}
}
Map<int, int> participants = Map<int, int>.from(chat['participants']);
Map<int, int> participants = _parseParticipants(chat['participants']);
return CachedChat(
id: id,
@@ -282,12 +296,12 @@ class ChatsModule {
static String? _nameFromContact(Map<dynamic, dynamic> 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?;
}
}
+6 -6
View File
@@ -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?;
}
+9 -10
View File
@@ -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<int>) 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<int>) 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<int>) return Uint8List.fromList(content);
return null;
} catch (e) {
return null;
+1
View File
@@ -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');
+23 -5
View File
@@ -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<int>? 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<Database>? _initCompleter;
static Future<Database> get _instance async {
_db ??= await _open();
if (_db != null) return _db!;
if (_initCompleter != null) return _initCompleter!.future;
_initCompleter = Completer<Database>();
try {
_db = await _open();
_initCompleter!.complete(_db!);
} catch (e) {
_initCompleter!.completeError(e);
_initCompleter = null;
rethrow;
}
return _db!;
}
+1 -1
View File
@@ -33,7 +33,7 @@ class TokenStorage {
static Future<String?> readActiveToken() async {
final id = await getActiveAccountId();
if (id == null) return null;
return readToken(id);
return await readToken(id);
}
static Future<void> deleteAccount(int accountId) async {
+3 -1
View File
@@ -99,7 +99,9 @@ class Connection {
if (socket != null) {
try {
socket.close();
} catch (_) {}
} catch (e) {
logger.w('Ошибка при закрытии сокета: $e');
}
}
_setState(SocketState.disconnected);
+17 -7
View File
@@ -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,
+1 -1
View File
@@ -8,7 +8,7 @@ class PacketSender {
int get currentSeq => _seq;
int _nextSeq() {
_seq = (_seq + 1) % 256;
_seq = (_seq + 1) % 65536;
return _seq;
}
@@ -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<ServerSettingsSheet> {
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)
@@ -638,6 +638,7 @@ class _ChatListScreenState extends State<ChatListScreen>
..removeListener(_onStoriesRevealTick)
..removeStatusListener(_onStoriesRevealStatus)
..dispose();
_shimmerController.dispose();
_folderPageController.dispose();
while (_folderChatScrollControllers.isNotEmpty) {
final c = _folderChatScrollControllers.removeLast();
+1 -1
View File
@@ -381,7 +381,7 @@ class _ChatScreenState extends State<ChatScreen>
myId: _myId,
prevMessage: prevMessage,
nextMessage: nextMessage,
chatType: chat!.type,
chatType: chat?.type ?? 'CHAT',
);
},
);
@@ -271,8 +271,9 @@ class _DevicesScreenState extends State<DevicesScreen>
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<DevicesScreen>
setState(() => _loadingIps.remove(id));
showCustomNotification(context, 'Ошибка IP: $e');
}
} finally {
client?.close();
}
}
+9 -4
View File
@@ -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<KometApp> {
late Locale _locale;
bool _isLoggingOut = false;
StreamSubscription<SessionExpiredException>? _sessionExpiredSub;
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
widget.initialFpsOverlay,
);
@@ -83,15 +86,16 @@ class KometAppState extends State<KometApp> {
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<KometApp> {
@override
void dispose() {
_sessionExpiredSub?.cancel();
fpsOverlayEnabled.dispose();
super.dispose();
}
+8 -4
View File
@@ -136,7 +136,8 @@ class VideoAttachment extends MessageAttachment {
} else if (previewRaw is List) {
try {
final bytes = List<int>.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<int>.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<int>.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<int>.from(previewRaw);
previewStr = String.fromCharCodes(bytes);
final base64 = String.fromCharCodes(bytes);
previewStr = 'data:image/webp;base64,$base64';
} catch (_) {}
}