feat: берсерк. Регенерация profile если сервер его не отдал

This commit is contained in:
Jganenokk
2026-07-15 19:33:25 +07:00
parent 1e2fc42b35
commit 0ba39b3575
12 changed files with 164 additions and 40 deletions
+34 -11
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import '../api.dart';
import '../../core/config/debug_test.dart';
import '../../core/config/komet_settings.dart';
import '../../core/protocol/chat_cache_fingerprint.dart';
import '../../core/protocol/opcode_map.dart';
@@ -41,6 +42,7 @@ class AccountModule {
late final ProfileModule _profile = ProfileModule(_api);
late final TwoFactorModule _twoFactor = TwoFactorModule(_api, _profile);
final _loginStatusController = StreamController<LoginStatus>.broadcast();
final _noticeController = StreamController<AccountNotice>.broadcast();
bool _loggedIn = false;
AccountModule(this._api) {
@@ -51,6 +53,8 @@ class AccountModule {
Stream<LoginStatus> get loginStatusStream => _loginStatusController.stream;
Stream<AccountNotice> get noticeStream => _noticeController.stream;
/// `true`, только когда сервер считает сессию ONLINE — после успешного
/// login (opcode 19), а не просто после хэндшейка (opcode 6).
bool get isLoggedIn => _loggedIn;
@@ -565,22 +569,16 @@ class AccountModule {
ProfileData profile;
final profileMap = data['profile'];
if (profileMap is Map) {
final contact = profileMap['contact'];
if (contact is! Map) {
throw Exception('login: отсутствует profile.contact в ответе');
}
if (!DebugTest.berserk &&
profileMap is Map &&
profileMap['contact'] is Map) {
profile = ProfileData.fromServerProfile(
profileMap.cast<dynamic, dynamic>(),
);
await AppDatabase.saveProfile(profile, isActive: true);
} else {
final cachedProfile = await AppDatabase.loadProfile(accountId);
if (cachedProfile == null) {
throw Exception('login: отсутствует profile в ответе');
}
profile = cachedProfile;
profile = await _resurrectProfile(accountId);
}
await AppDatabase.saveProfile(profile, isActive: true);
await AppDatabase.setActiveAccount(profile.id);
await _saveSyncState(data, serverTime, profile.id);
@@ -625,6 +623,31 @@ class AccountModule {
);
}
Future<ProfileData> _resurrectProfile(int accountId) async {
if (DebugTest.berserk) {
await AppDatabase.deleteAccount(accountId);
logger.w('login: [BERSERK] профиль удалён из БД, форсирую регенерацию (id=$accountId)');
} else {
final cached = await AppDatabase.loadProfile(accountId);
if (cached != null) return cached;
}
_noticeController.add(AccountNotice.resurrectingProfile);
try {
final fetched = await ContactsModule.fetchSelfProfile(_api, accountId);
if (fetched != null) {
logger.i('login: профиль восстановлен через CONTACT_INFO (id=$accountId)');
return fetched;
}
} catch (e) {
logger.w('login: восстановление профиля через CONTACT_INFO не удалось: $e');
}
logger.w('login: профиль недоступен, использую заглушку (id=$accountId)');
return ProfileData.stub(accountId);
}
Future<void> _saveSyncState(
Map<dynamic, dynamic> data,
int serverTime,
@@ -282,6 +282,8 @@ enum AuthRequestType {
enum LoginStatus { idle, loading, success, error }
enum AccountNotice { resurrectingProfile }
class WrongDeviceTokenException implements Exception {
const WrongDeviceTokenException();
@override
+15
View File
@@ -191,6 +191,21 @@ class ContactsModule {
await syncFromLoginPayload(map.cast<dynamic, dynamic>(), accountId);
}
static Future<ProfileData?> fetchSelfProfile(Api api, int accountId) async {
final map = await api.sendRequestMap(Opcode.contactInfo, {
'contactIds': [accountId],
});
final contacts = map?['contacts'];
if (contacts is! List) return null;
for (final raw in contacts.whereType<Map>()) {
if (raw['id'] != accountId) continue;
final contact = raw.cast<dynamic, dynamic>();
_primeContactCache(contact);
return ProfileData.fromServerMap(contact);
}
return null;
}
static void _primeContactCache(Map<dynamic, dynamic> contact) {
final id = contact['id'];
if (id is! int) return;