руки переломаю
This commit is contained in:
+9
-1
@@ -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.*
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// Внутрянка
|
||||
|
||||
@@ -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>?)
|
||||
|
||||
@@ -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('Не удалось получить обновлённый профиль');
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -99,7 +99,9 @@ class Connection {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
logger.w('Ошибка при закрытии сокета: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_setState(SocketState.disconnected);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user