удаление чатов и фикс загрузки ботов в chatsэ

This commit is contained in:
Jganenok
2026-05-17 18:29:28 +07:00
parent 74bad21635
commit 3aa6cdb52c
6 changed files with 841 additions and 364 deletions
+191 -9
View File
@@ -1,12 +1,15 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../core/storage/app_database.dart';
import '../../core/storage/token_storage.dart';
import '../../core/utils/logger.dart';
import '../api.dart';
import 'messages.dart' show ContactCache;
Map<int, int> _parseParticipants(dynamic raw) {
try {
@@ -43,6 +46,8 @@ class CachedChat {
final int seenTime;
final Map<int, int> participants;
final Set<String> options;
final int? owner;
final Set<int> admins;
CachedChat({
required this.id,
@@ -63,12 +68,16 @@ class CachedChat {
required this.seenTime,
required this.participants,
this.options = const {},
this.owner,
this.admins = const {},
}) : lastMsgTextOneLine = lastMsgText != null && lastMsgText.contains('\n')
? lastMsgText.replaceAll('\n', ' ')
: lastMsgText;
bool get isOfficial => options.contains('OFFICIAL');
bool iAmAdmin(int myId) => owner == myId || admins.contains(myId);
factory CachedChat.fromDbRow(Map<String, dynamic> row) => CachedChat(
id: row['id'] as int,
accountId: row['account_id'] as int,
@@ -88,6 +97,8 @@ class CachedChat {
seenTime: row['seen_time'] as int,
participants: _parseParticipants(row['participants']),
options: _decodeOptions(row['options']),
owner: row['owner'] as int?,
admins: _decodeAdmins(row['admins']),
);
static Set<String> _decodeOptions(dynamic raw) {
@@ -95,6 +106,15 @@ class CachedChat {
return raw.split(',').where((s) => s.isNotEmpty).toSet();
}
static Set<int> _decodeAdmins(dynamic raw) {
if (raw is! String || raw.isEmpty) return const {};
return raw
.split(',')
.map((s) => int.tryParse(s.trim()))
.whereType<int>()
.toSet();
}
Map<String, dynamic> toDbRow() => {
'id': id,
'account_id': accountId,
@@ -114,6 +134,8 @@ class CachedChat {
'seen_time': seenTime,
'participants': jsonEncode(participants.map((k, v) => MapEntry(k.toString(), v))),
'options': options.isEmpty ? null : options.join(','),
'owner': owner,
'admins': admins.isEmpty ? null : admins.join(','),
};
}
@@ -121,15 +143,68 @@ class ChatsModule {
static final ValueNotifier<int> chatsChanged = ValueNotifier(0);
static void _bump() => chatsChanged.value = chatsChanged.value + 1;
static final Set<int> _pendingContactUpdates = {};
static Timer? _contactFlushTimer;
static const _contactFlushDelay = Duration(milliseconds: 250);
static void applyContactUpdate(int contactId) {
_pendingContactUpdates.add(contactId);
_contactFlushTimer ??= Timer(_contactFlushDelay, _flushContactUpdates);
}
static Future<void> _flushContactUpdates() async {
_contactFlushTimer = null;
if (_pendingContactUpdates.isEmpty) return;
final ids = _pendingContactUpdates.toList();
_pendingContactUpdates.clear();
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) return;
final updates = <Map<String, dynamic>>[];
for (final contactId in ids) {
final name = ContactCache.get(contactId);
if (name == null) continue;
final avatar = ContactCache.getAvatar(contactId);
final options = ContactCache.getOptions(contactId) ?? const <String>{};
final rows = await AppDatabase.findDialogChatsByParticipant(
accountId,
contactId,
);
for (final row in rows) {
final cached = CachedChat.fromDbRow(row);
final sameTitle = cached.title == name;
final sameAvatar = (cached.iconUrl ?? '') == (avatar ?? '');
final sameOptions = cached.options.length == options.length &&
cached.options.containsAll(options);
if (sameTitle && sameAvatar && sameOptions) continue;
final newRow = Map<String, dynamic>.from(row);
newRow['title'] = name;
newRow['icon_url'] = avatar;
newRow['options'] = options.isEmpty ? null : options.join(',');
updates.add(newRow);
}
}
if (updates.isNotEmpty) {
await AppDatabase.saveChats(updates);
_bump();
}
}
static Future<CachedChat?> cacheServerChat(
Map<dynamic, dynamic> chat,
int accountId,
) async {
final cachedAt = DateTime.now().millisecondsSinceEpoch;
final existingRows = await AppDatabase.loadChats(accountId);
final existing = {
for (final row in existingRows) row['id'] as int: CachedChat.fromDbRow(row),
};
final id = chat['id'];
Map<int, CachedChat> existing = const {};
if (id is int) {
final rows = await AppDatabase.loadChat(accountId, id);
if (rows.isNotEmpty) {
existing = {id: CachedChat.fromDbRow(rows.first)};
}
}
final parsed = _parseChat(
chat,
accountId,
@@ -140,7 +215,10 @@ class ChatsModule {
existing,
cachedAt,
);
if (parsed == null) return null;
if (parsed == null) {
logger.w('cacheServerChat: parse returned null for chat=${chat['id']}');
return null;
}
await AppDatabase.saveChats([parsed.toDbRow()]);
_bump();
return parsed;
@@ -306,6 +384,12 @@ class ChatsModule {
if (config is Map) {
favIndex = config['favIndex'] as int?;
dontDisturbUntil = (config['dontDisturbUntil'] as int?) ?? 0;
} else {
final ex = existing[id];
if (ex != null) {
favIndex = ex.favIndex;
dontDisturbUntil = ex.dontDisturbUntil;
}
}
@@ -320,6 +404,31 @@ class ChatsModule {
}
Map<int, int> participants = _parseParticipants(chat['participants']);
int? owner;
final ownerRaw = chat['owner'];
if (ownerRaw is int) {
owner = ownerRaw;
} else if (ownerRaw is String) {
owner = int.tryParse(ownerRaw);
}
Set<int> admins = const {};
final adminsRaw = chat['admins'];
if (adminsRaw is List) {
admins = adminsRaw
.map((e) => e is int ? e : int.tryParse(e.toString()))
.whereType<int>()
.toSet();
} else {
final adminParticipants = chat['adminParticipants'];
if (adminParticipants is Map) {
admins = adminParticipants.keys
.map((k) => k is int ? k : int.tryParse(k.toString()))
.whereType<int>()
.toSet();
}
}
return CachedChat(
id: id,
accountId: accountId,
@@ -339,6 +448,8 @@ class ChatsModule {
seenTime: seenTime,
participants: participants,
options: options,
owner: owner,
admins: admins,
);
} catch (e) {
logger.e("Ошибка при парсинге чата: $e");
@@ -410,13 +521,25 @@ class ChatsModule {
'notify': notify,
};
final packet = await api.sendRequest(Opcode.msgSend, payload);
if (!packet.isOk) return null;
if (!packet.isOk) {
logger.w('createGroupChat: server error payload=${packet.payload}');
return null;
}
final data = packet.payload;
if (data is! Map) return null;
if (data is! Map) {
logger.w('createGroupChat: payload is not a Map: $data');
return null;
}
final chat = data['chat'];
if (chat is! Map) return null;
if (chat is! Map) {
logger.w('createGroupChat: response has no chat field: $data');
return null;
}
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) return null;
if (accountId == null) {
logger.w('createGroupChat: no active account id');
return null;
}
return cacheServerChat(chat, accountId);
}
@@ -439,4 +562,63 @@ class ChatsModule {
});
return packet.isOk;
}
static Future<String?> deleteChat(
Api api, {
required int chatId,
required int lastEventTime,
required bool forAll,
}) async {
try {
await api.sendRequest(Opcode.chatDelete, {
'chatId': chatId,
'lastEventTime': lastEventTime,
'forAll': forAll,
});
final accountId = await TokenStorage.getActiveAccountId();
if (accountId != null) {
await AppDatabase.deleteChat(chatId, accountId);
_bump();
}
return null;
} on PacketError catch (e) {
logger.w('deleteChat $chatId: ${e.message}');
return e.message;
} catch (e) {
logger.w('deleteChat $chatId: $e');
return 'Не удалось удалить чат';
}
}
static Future<List<CachedChat>> refreshChats(
Api api,
List<int> chatIds,
) async {
if (chatIds.isEmpty) return const [];
try {
final packet = await api.sendRequest(Opcode.chatInfo, {
'chatIds': chatIds,
});
final payload = packet.payload;
if (payload is! Map) return const [];
final list = payload['chats'];
if (list is! List) return const [];
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) return const [];
final out = <CachedChat>[];
for (final c in list) {
if (c is Map) {
final cached = await cacheServerChat(c, accountId);
if (cached != null) out.add(cached);
}
}
return out;
} on PacketError catch (e) {
logger.w('refreshChats: ${e.message}');
return const [];
} catch (e) {
logger.w('refreshChats: $e');
return const [];
}
}
}
+92 -9
View File
@@ -7,6 +7,7 @@ import '../api.dart';
import '../../core/config/proxy_config.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/transport/proxy_connector.dart';
import '../../core/utils/logger.dart';
import 'messages.dart';
sealed class UploadEvent {
@@ -189,7 +190,12 @@ class FileUploader {
Socket? socket;
try {
socket = await _openSocket(uri);
_writeHeaders(socket, uri, filename, bytes.length, contentType: 'image/jpeg');
_writeImageHeaders(
socket,
uri,
bytes.length,
contentType: _contentTypeForFilename(filename),
);
socket.add(bytes);
await socket.flush();
@@ -201,11 +207,22 @@ class FileUploader {
socket.destroy();
} catch (_) {}
if (response == null) return null;
if (response == null) {
logger.w('uploadImage: empty/timed-out response');
return null;
}
final (status, body) = response;
if (status != 200) return null;
return _parsePhotoToken(body);
} catch (_) {
if (status != 200) {
logger.w('uploadImage: status=$status body=${body.length > 200 ? '${body.substring(0, 200)}' : body}');
return null;
}
final token = _parsePhotoToken(body);
if (token == null) {
logger.w('uploadImage: photoToken not found in body=${body.length > 200 ? '${body.substring(0, 200)}' : body}');
}
return token;
} catch (e) {
logger.w('uploadImage: $e');
try {
socket?.destroy();
} catch (_) {}
@@ -213,6 +230,40 @@ class FileUploader {
}
}
void _writeImageHeaders(Socket socket, Uri uri, int total, {required String contentType}) {
final path = '${uri.path}${uri.hasQuery ? "?${uri.query}" : ""}';
final headers = StringBuffer()
..write('POST $path HTTP/1.1\r\n')
..write('Host: ${uri.host}\r\n')
..write('Content-Type: $contentType\r\n')
..write('Content-Length: $total\r\n')
..write('Connection: keep-alive\r\n')
..write('User-Agent: ${Uri.encodeComponent('OKMessages/26.14.1 (Android 11; TECNO MOBILE LIMITED TECNO LE7n; xxhdpi 480dpi 1080x2208)')}\r\n')
..write('\r\n');
socket.add(utf8.encode(headers.toString()));
}
String _contentTypeForFilename(String filename) {
final ext = filename.contains('.') ? filename.split('.').last.toLowerCase() : '';
switch (ext) {
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'webp':
return 'image/webp';
case 'heic':
case 'heif':
return 'image/heic';
case 'bmp':
return 'image/bmp';
case 'jpg':
case 'jpeg':
default:
return 'image/jpeg';
}
}
Future<(int, String)?> _readFullResponse(
Socket socket, {
required Duration timeout,
@@ -232,10 +283,15 @@ class FileUploader {
return;
}
final headerStr = utf8.decode(bytes.sublist(0, headerEnd), allowMalformed: true);
final statusLine = headerStr.split('\r\n').first;
final parts = statusLine.split(' ');
final lines = headerStr.split('\r\n');
final parts = lines.first.split(' ');
final status = parts.length >= 2 ? (int.tryParse(parts[1]) ?? 0) : 0;
final body = utf8.decode(bytes.sublist(headerEnd), allowMalformed: true);
final chunked = lines.skip(1).any(
(l) => l.toLowerCase().startsWith('transfer-encoding:') &&
l.toLowerCase().contains('chunked'),
);
final rawBody = utf8.decode(bytes.sublist(headerEnd), allowMalformed: true);
final body = chunked ? _decodeChunked(rawBody) : rawBody;
completer.complete((status, body));
}
@@ -250,6 +306,31 @@ class FileUploader {
return completer.future;
}
String _decodeChunked(String body) {
final out = StringBuffer();
var i = 0;
while (i < body.length) {
final lineEnd = body.indexOf('\r\n', i);
if (lineEnd < 0) break;
final sizeStr = body.substring(i, lineEnd).split(';').first.trim();
if (sizeStr.isEmpty) {
i = lineEnd + 2;
continue;
}
final size = int.tryParse(sizeStr, radix: 16);
if (size == null) break;
if (size == 0) break;
final dataStart = lineEnd + 2;
if (dataStart + size > body.length) break;
out.write(body.substring(dataStart, dataStart + size));
i = dataStart + size;
if (i + 2 <= body.length && body.substring(i, i + 2) == '\r\n') {
i += 2;
}
}
return out.toString();
}
String? _parsePhotoToken(String body) {
try {
final json = jsonDecode(body);
@@ -266,7 +347,9 @@ class FileUploader {
final pt = json['photoToken'];
if (pt is String && pt.isNotEmpty) return pt;
}
} catch (_) {}
} catch (e) {
logger.w('parsePhotoToken: $e');
}
return null;
}
+3
View File
@@ -5,6 +5,7 @@ import '../api.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/storage/app_database.dart';
import '../../models/attachment.dart';
import 'chats.dart' show ChatsModule;
class ContactCache {
static final Map<int, String> _nameCache = {};
@@ -21,6 +22,7 @@ class ContactCache {
static String? get(int id) => _nameCache[id];
static String? getAvatar(int id) => _avatarCache[id];
static Set<String>? getOptions(int id) => _optionsCache[id];
static bool isOfficial(int id) => _optionsCache[id]?.contains('OFFICIAL') ?? false;
}
@@ -641,6 +643,7 @@ class MessagesModule {
ContactCache.putOptions(contactId, rawOpts.whereType<String>().toSet());
}
ChatsModule.applyContactUpdate(contactId);
return fullName;
}
}