feat: гост мод и фиксы разные всякие я линус торвальдс
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../api.dart';
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/protocol/chat_cache_fingerprint.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
@@ -1146,7 +1147,7 @@ class AccountModule {
|
||||
) {
|
||||
final payload = <dynamic, dynamic>{
|
||||
'token': token,
|
||||
'interactive': true,
|
||||
'interactive': !KometSettings.ghostMode.value,
|
||||
'exp': {
|
||||
'chatsCountGroups': Uint8List.fromList([0x0b, 0x32]),
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
import '../../core/cache/info_cache.dart';
|
||||
@@ -180,6 +181,11 @@ class MessageRemovedEvent extends MessageEvent {
|
||||
const MessageRemovedEvent(super.chatId, this.messageId);
|
||||
}
|
||||
|
||||
class MessageMarkedDeletedEvent extends MessageEvent {
|
||||
final String messageId;
|
||||
const MessageMarkedDeletedEvent(super.chatId, this.messageId);
|
||||
}
|
||||
|
||||
class MessageReactionsChangedEvent extends MessageEvent {
|
||||
final String messageId;
|
||||
final Map<String, dynamic>? reactionInfo;
|
||||
@@ -267,6 +273,11 @@ class ChatsModule {
|
||||
String messageId,
|
||||
int mark,
|
||||
) async {
|
||||
final rows = await AppDatabase.loadChat(accountId, chatId);
|
||||
if (rows.isEmpty) return;
|
||||
final row = Map<String, dynamic>.from(rows.first);
|
||||
if ((row['unread_count'] as int? ?? 0) == 0) return;
|
||||
|
||||
final msgIdNum = int.tryParse(messageId);
|
||||
if (msgIdNum != null) {
|
||||
try {
|
||||
@@ -279,10 +290,6 @@ class ChatsModule {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final rows = await AppDatabase.loadChat(accountId, chatId);
|
||||
if (rows.isEmpty) return;
|
||||
final row = Map<String, dynamic>.from(rows.first);
|
||||
if ((row['unread_count'] as int? ?? 0) == 0) return;
|
||||
row['unread_count'] = 0;
|
||||
await AppDatabase.saveChats([row]);
|
||||
_bump();
|
||||
@@ -367,9 +374,21 @@ class ChatsModule {
|
||||
await _handleNotifMsgReactionsChanged(packet);
|
||||
case Opcode.notifMsgDelete:
|
||||
await _handleNotifMsgDelete(packet);
|
||||
case Opcode.notifPresence:
|
||||
_handlePresence(packet);
|
||||
}
|
||||
}
|
||||
|
||||
static void _handlePresence(Packet packet) {
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
final userId = payload['userId'];
|
||||
if (userId is! int) return;
|
||||
final presence = payload['presence'];
|
||||
if (presence is! Map) return;
|
||||
PresenceFetch.apply(userId, Map<String, dynamic>.from(presence));
|
||||
}
|
||||
|
||||
static Future<void> _handleNotifMsgDelete(Packet packet) async {
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
@@ -386,13 +405,19 @@ class ChatsModule {
|
||||
}
|
||||
if (chatId == null) return;
|
||||
|
||||
final keepDeleted = KometSettings.viewDeleted.value;
|
||||
final ids = payload['messageIds'];
|
||||
if (ids is List) {
|
||||
for (final raw in ids) {
|
||||
final id = raw?.toString();
|
||||
if (id == null || id.isEmpty) continue;
|
||||
await AppDatabase.deleteMessage(accountId, chatId, id);
|
||||
_messageEventsController.add(MessageRemovedEvent(chatId, id));
|
||||
if (keepDeleted) {
|
||||
await AppDatabase.markMessageDeleted(accountId, chatId, id);
|
||||
_messageEventsController.add(MessageMarkedDeletedEvent(chatId, id));
|
||||
} else {
|
||||
await AppDatabase.deleteMessage(accountId, chatId, id);
|
||||
_messageEventsController.add(MessageRemovedEvent(chatId, id));
|
||||
}
|
||||
}
|
||||
}
|
||||
_bump();
|
||||
@@ -435,7 +460,12 @@ class ChatsModule {
|
||||
}
|
||||
|
||||
if (status == 'REMOVED' && msgIdStr != null) {
|
||||
await AppDatabase.deleteMessage(accountId, chatId, msgIdStr);
|
||||
final keepDeleted = KometSettings.viewDeleted.value;
|
||||
if (keepDeleted) {
|
||||
await AppDatabase.markMessageDeleted(accountId, chatId, msgIdStr);
|
||||
} else {
|
||||
await AppDatabase.deleteMessage(accountId, chatId, msgIdStr);
|
||||
}
|
||||
final cachedChat = CachedChat.fromDbRow(rows.first);
|
||||
if (cachedChat.lastMsgId == msgIdInt) {
|
||||
await _reconcileLastMessage(accountId, chatId, rows.first, unread: unread);
|
||||
@@ -444,7 +474,11 @@ class ChatsModule {
|
||||
newRow['unread_count'] = unread;
|
||||
await AppDatabase.saveChats([newRow]);
|
||||
}
|
||||
_messageEventsController.add(MessageRemovedEvent(chatId, msgIdStr));
|
||||
_messageEventsController.add(
|
||||
keepDeleted
|
||||
? MessageMarkedDeletedEvent(chatId, msgIdStr)
|
||||
: MessageRemovedEvent(chatId, msgIdStr),
|
||||
);
|
||||
_bump();
|
||||
return;
|
||||
}
|
||||
@@ -523,7 +557,12 @@ class ChatsModule {
|
||||
Map<String, dynamic> chatRow, {
|
||||
int? unread,
|
||||
}) async {
|
||||
final latest = await AppDatabase.loadMessages(accountId, chatId, limit: 1);
|
||||
final latest = await AppDatabase.loadMessages(
|
||||
accountId,
|
||||
chatId,
|
||||
limit: 1,
|
||||
onlyVisible: true,
|
||||
);
|
||||
final newRow = Map<String, dynamic>.from(chatRow);
|
||||
if (latest.isNotEmpty) {
|
||||
final m = latest.first;
|
||||
@@ -576,6 +615,54 @@ class ChatsModule {
|
||||
_bump();
|
||||
}
|
||||
|
||||
static Future<List<String>> reconcileDeletedFromFetch(
|
||||
int accountId,
|
||||
int chatId,
|
||||
List<CachedMessage> serverMessages,
|
||||
) async {
|
||||
if (serverMessages.isEmpty) return const [];
|
||||
|
||||
final serverIds = <String>{};
|
||||
var minTime = serverMessages.first.time;
|
||||
var maxTime = serverMessages.first.time;
|
||||
for (final m in serverMessages) {
|
||||
serverIds.add(m.id);
|
||||
if (m.time < minTime) minTime = m.time;
|
||||
if (m.time > maxTime) maxTime = m.time;
|
||||
}
|
||||
|
||||
final cached = await AppDatabase.loadMessages(
|
||||
accountId,
|
||||
chatId,
|
||||
limit: 300,
|
||||
onlyVisible: true,
|
||||
);
|
||||
|
||||
final newlyDeleted = <String>[];
|
||||
for (final row in cached) {
|
||||
final id = row['id']?.toString();
|
||||
if (id == null || id.isEmpty || id.startsWith('temp_')) continue;
|
||||
if (serverIds.contains(id)) continue;
|
||||
|
||||
final status = row['status']?.toString();
|
||||
if (status == 'pending' || status == 'sending' || status == 'error') {
|
||||
continue;
|
||||
}
|
||||
|
||||
final time = row['time'] is int
|
||||
? row['time'] as int
|
||||
: int.tryParse(row['time']?.toString() ?? '') ?? 0;
|
||||
if (time < minTime || time > maxTime) continue;
|
||||
|
||||
newlyDeleted.add(id);
|
||||
}
|
||||
|
||||
if (newlyDeleted.isNotEmpty) {
|
||||
await AppDatabase.markMessagesDeleted(accountId, chatId, newlyDeleted);
|
||||
}
|
||||
return newlyDeleted;
|
||||
}
|
||||
|
||||
static Future<void> _handleNotifMsgReactionsChanged(Packet packet) async {
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -14,13 +15,48 @@ class ContactCache {
|
||||
static final Map<int, String> _avatarCache = {};
|
||||
static final Map<int, Set<String>> _optionsCache = {};
|
||||
|
||||
static void put(int id, String name) => _nameCache[id] = name;
|
||||
static const _prefsKey = 'contact_cache_v1';
|
||||
static Timer? _saveTimer;
|
||||
static bool _loaded = false;
|
||||
|
||||
static void putAvatar(int id, String? baseUrl) {
|
||||
if (baseUrl != null) _avatarCache[id] = baseUrl;
|
||||
static Future<void> load() async {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_prefsKey);
|
||||
if (raw == null) return;
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) return;
|
||||
decoded.forEach((key, value) {
|
||||
final id = int.tryParse(key.toString());
|
||||
if (id == null || value is! Map) return;
|
||||
final name = value['n'];
|
||||
final avatar = value['a'];
|
||||
final opts = value['o'];
|
||||
if (name is String) _nameCache[id] = name;
|
||||
if (avatar is String) _avatarCache[id] = avatar;
|
||||
if (opts is List) _optionsCache[id] = opts.whereType<String>().toSet();
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static void putOptions(int id, Set<String> opts) => _optionsCache[id] = opts;
|
||||
static void put(int id, String name) {
|
||||
_nameCache[id] = name;
|
||||
_scheduleSave();
|
||||
}
|
||||
|
||||
static void putAvatar(int id, String? baseUrl) {
|
||||
if (baseUrl != null) {
|
||||
_avatarCache[id] = baseUrl;
|
||||
_scheduleSave();
|
||||
}
|
||||
}
|
||||
|
||||
static void putOptions(int id, Set<String> opts) {
|
||||
_optionsCache[id] = opts;
|
||||
_scheduleSave();
|
||||
}
|
||||
|
||||
static String? get(int id) => _nameCache[id];
|
||||
static String? getAvatar(int id) => _avatarCache[id];
|
||||
@@ -32,6 +68,40 @@ class ContactCache {
|
||||
_nameCache.clear();
|
||||
_avatarCache.clear();
|
||||
_optionsCache.clear();
|
||||
_saveTimer?.cancel();
|
||||
_saveTimer = null;
|
||||
unawaited(_wipePersisted());
|
||||
}
|
||||
|
||||
static void _scheduleSave() {
|
||||
_saveTimer?.cancel();
|
||||
_saveTimer = Timer(const Duration(seconds: 3), () => unawaited(_save()));
|
||||
}
|
||||
|
||||
static Future<void> _save() async {
|
||||
final ids = <int>{
|
||||
..._nameCache.keys,
|
||||
..._avatarCache.keys,
|
||||
..._optionsCache.keys,
|
||||
};
|
||||
final map = <String, dynamic>{};
|
||||
for (final id in ids) {
|
||||
final entry = <String, dynamic>{};
|
||||
final name = _nameCache[id];
|
||||
final avatar = _avatarCache[id];
|
||||
final opts = _optionsCache[id];
|
||||
if (name != null) entry['n'] = name;
|
||||
if (avatar != null) entry['a'] = avatar;
|
||||
if (opts != null && opts.isNotEmpty) entry['o'] = opts.toList();
|
||||
if (entry.isNotEmpty) map['$id'] = entry;
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsKey, jsonEncode(map));
|
||||
}
|
||||
|
||||
static Future<void> _wipePersisted() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefsKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +267,7 @@ class CachedMessage {
|
||||
final Map<String, dynamic>? payload;
|
||||
final List<MessageAttachment>? attachments;
|
||||
final bool isControl;
|
||||
final bool deleted;
|
||||
|
||||
const CachedMessage({
|
||||
required this.id,
|
||||
@@ -209,8 +280,27 @@ class CachedMessage {
|
||||
this.payload,
|
||||
this.attachments,
|
||||
this.isControl = false,
|
||||
this.deleted = false,
|
||||
});
|
||||
|
||||
CachedMessage copyWith({
|
||||
String? status,
|
||||
bool? deleted,
|
||||
List<MessageAttachment>? attachments,
|
||||
}) => CachedMessage(
|
||||
id: id,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: senderId,
|
||||
text: text,
|
||||
time: time,
|
||||
status: status ?? this.status,
|
||||
payload: payload,
|
||||
attachments: attachments ?? this.attachments,
|
||||
isControl: isControl,
|
||||
deleted: deleted ?? this.deleted,
|
||||
);
|
||||
|
||||
factory CachedMessage.fromDbRow(Map<String, dynamic> row) {
|
||||
Map<String, dynamic>? payload;
|
||||
final payloadRaw = row['payload'];
|
||||
@@ -259,6 +349,9 @@ class CachedMessage {
|
||||
attachments: attachments,
|
||||
isControl:
|
||||
attachments?.any((a) => a.type == AttachmentType.control) ?? false,
|
||||
deleted: row['deleted'] is int
|
||||
? row['deleted'] == 1
|
||||
: row['deleted']?.toString() == '1',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,6 +388,7 @@ class CachedMessage {
|
||||
'time': time,
|
||||
'status': status,
|
||||
'payload': payload != null ? jsonEncode(payload) : null,
|
||||
'deleted': deleted ? 1 : 0,
|
||||
};
|
||||
|
||||
static CachedMessage fromPushPayload(int accountId, int chatId, Map msg) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../core/cache/info_cache.dart';
|
||||
import '../../core/cache/self_presence.dart';
|
||||
import '../../core/config/komet_settings.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../api.dart';
|
||||
|
||||
class SelfCheckService {
|
||||
SelfCheckService._();
|
||||
|
||||
static final SelfCheckService instance = SelfCheckService._();
|
||||
|
||||
static const Duration interval = Duration(seconds: 10);
|
||||
|
||||
Api? _api;
|
||||
Timer? _timer;
|
||||
|
||||
void init(Api api) {
|
||||
if (_api != null) return;
|
||||
_api = api;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(interval, (_) => unawaited(_check()));
|
||||
}
|
||||
|
||||
Future<void> _check() async {
|
||||
final api = _api;
|
||||
if (api == null || api.state != SessionState.online) return;
|
||||
if (!KometSettings.selfOnlineCheck.value) return;
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return;
|
||||
final presence = await PresenceFetch.get(accountId, forceRefresh: true);
|
||||
logger.i('[SELF CHECK] id=$accountId presence=$presence');
|
||||
if (presence == null) return;
|
||||
SelfPresence.applySelfCheck(
|
||||
online: presence['status'] == 1,
|
||||
seenSeconds: presence['seen'] is int ? presence['seen'] as int : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user