feat: берсерк. Регенерация profile если сервер его не отдал
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/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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
class DebugTest {
|
||||
static bool enabled = false;
|
||||
static int contactCount = 0;
|
||||
static bool berserk = false;
|
||||
|
||||
static const int debugAccountId = -424242;
|
||||
|
||||
static const bool _envEnabled = bool.fromEnvironment('DEBUG_TEST');
|
||||
static const bool _envBerserk = bool.fromEnvironment('BERSERK');
|
||||
static const int _envContacts = int.fromEnvironment(
|
||||
'DEBUG_CONTACTS',
|
||||
defaultValue: -1,
|
||||
);
|
||||
|
||||
static const String _flag = '--debug-test';
|
||||
static const String _berserkFlag = '--berserk';
|
||||
static const String _contactsFlag = '--contacts';
|
||||
|
||||
static void parse(List<String> args) {
|
||||
if (_envEnabled) enabled = true;
|
||||
if (_envBerserk) berserk = true;
|
||||
if (_envContacts >= 0) {
|
||||
enabled = true;
|
||||
contactCount = _envContacts;
|
||||
@@ -24,6 +28,8 @@ class DebugTest {
|
||||
final arg = args[i];
|
||||
if (arg == _flag) {
|
||||
enabled = true;
|
||||
} else if (arg == _berserkFlag) {
|
||||
berserk = true;
|
||||
} else if (arg.startsWith('$_contactsFlag=')) {
|
||||
enabled = true;
|
||||
contactCount =
|
||||
|
||||
@@ -36,6 +36,15 @@ class ProfileData {
|
||||
this.profileOptions,
|
||||
});
|
||||
|
||||
factory ProfileData.stub(int id) => ProfileData(
|
||||
id: id,
|
||||
firstName: '',
|
||||
phone: 0,
|
||||
country: '',
|
||||
accountStatus: 0,
|
||||
updateTime: 0,
|
||||
);
|
||||
|
||||
factory ProfileData.fromServerProfile(Map<dynamic, dynamic> profile) {
|
||||
final contact = profile['contact'];
|
||||
if (contact is! Map) {
|
||||
@@ -70,7 +79,7 @@ class ProfileData {
|
||||
id: contact['id'] as int,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phone: contact['phone'] as int,
|
||||
phone: (contact['phone'] as int?) ?? 0,
|
||||
photoId: contact['photoId'] as int?,
|
||||
baseUrl: contact['baseUrl'] as String?,
|
||||
baseRawUrl: contact['baseRawUrl'] as String?,
|
||||
|
||||
@@ -340,7 +340,9 @@ class _SettingsTabState extends State<SettingsTab> {
|
||||
|
||||
final String fullName =
|
||||
'${_profile!.firstName}${_profile!.lastName != null ? ' ${_profile!.lastName}' : ''}';
|
||||
final String phone = '+${_profile!.phone}';
|
||||
final String phone = _profile!.phone == 0
|
||||
? l10n.profilePhoneRegenFailed
|
||||
: '+${_profile!.phone}';
|
||||
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final topPad = MediaQuery.paddingOf(context).top;
|
||||
@@ -672,6 +674,7 @@ class _SettingsTabState extends State<SettingsTab> {
|
||||
) {
|
||||
final topPad = MediaQuery.paddingOf(context).top;
|
||||
final hasPhoto = (_profile?.baseUrl ?? '').isNotEmpty;
|
||||
final phoneMissing = (_profile?.phone ?? 0) == 0;
|
||||
final pt = hasPhoto ? t : 0.0;
|
||||
if (pt > 0) _headerEverExpanded = true;
|
||||
|
||||
@@ -826,37 +829,57 @@ class _SettingsTabState extends State<SettingsTab> {
|
||||
const SizedBox(height: 6),
|
||||
_headerAligned(
|
||||
pt,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => setState(
|
||||
() => _isPhoneVisible = !_isPhoneVisible,
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: _PhoneSpoiler(
|
||||
text: phone,
|
||||
isVisible: _isPhoneVisible,
|
||||
phoneMissing
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Text(
|
||||
phone,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: subColor,
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.5,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => setState(
|
||||
() => _isPhoneVisible = !_isPhoneVisible,
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: _PhoneSpoiler(
|
||||
text: phone,
|
||||
isVisible: _isPhoneVisible,
|
||||
style: TextStyle(
|
||||
color: subColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_isPhoneVisible
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off,
|
||||
size: 14,
|
||||
color: Color.lerp(
|
||||
cs.mutedText,
|
||||
Colors.white70,
|
||||
pt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_isPhoneVisible
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off,
|
||||
size: 14,
|
||||
color: Color.lerp(cs.mutedText, Colors.white70, pt),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+3
-1
@@ -907,5 +907,7 @@
|
||||
"updateCheck": "Check for updates",
|
||||
"updateChecking": "Checking for updates…",
|
||||
"updateUpToDate": "You have the latest version",
|
||||
"updateCheckFailed": "Couldn't check for updates. Try again later"
|
||||
"updateCheckFailed": "Couldn't check for updates. Try again later",
|
||||
"profileResurrecting": "Oops! The server didn't send your profile. Trying to regenerate…",
|
||||
"profilePhoneRegenFailed": "Couldn't regenerate your phone number. Please sign in again and report the issue to the developers"
|
||||
}
|
||||
|
||||
@@ -4045,6 +4045,18 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t check for updates. Try again later'**
|
||||
String get updateCheckFailed;
|
||||
|
||||
/// No description provided for @profileResurrecting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Oops! The server didn\'t send your profile. Trying to regenerate…'**
|
||||
String get profileResurrecting;
|
||||
|
||||
/// No description provided for @profilePhoneRegenFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'**
|
||||
String get profilePhoneRegenFailed;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -2097,4 +2097,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get updateCheckFailed =>
|
||||
'Couldn\'t check for updates. Try again later';
|
||||
|
||||
@override
|
||||
String get profileResurrecting =>
|
||||
'Oops! The server didn\'t send your profile. Trying to regenerate…';
|
||||
|
||||
@override
|
||||
String get profilePhoneRegenFailed =>
|
||||
'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers';
|
||||
}
|
||||
|
||||
@@ -2108,4 +2108,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get updateCheckFailed =>
|
||||
'Не удалось проверить обновления. Повторите позже';
|
||||
|
||||
@override
|
||||
String get profileResurrecting =>
|
||||
'Упс! Сервер не прислал profile. Попробую регенерировать…';
|
||||
|
||||
@override
|
||||
String get profilePhoneRegenFailed =>
|
||||
'Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам';
|
||||
}
|
||||
|
||||
+3
-1
@@ -689,5 +689,7 @@
|
||||
"updateCheck": "Проверить обновление",
|
||||
"updateChecking": "Проверяем обновления…",
|
||||
"updateUpToDate": "Установлена актуальная версия",
|
||||
"updateCheckFailed": "Не удалось проверить обновления. Повторите позже"
|
||||
"updateCheckFailed": "Не удалось проверить обновления. Повторите позже",
|
||||
"profileResurrecting": "Упс! Сервер не прислал profile. Попробую регенерировать…",
|
||||
"profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам"
|
||||
}
|
||||
|
||||
@@ -339,6 +339,7 @@ class KometAppState extends State<KometApp>
|
||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||
StreamSubscription<IncomingCall>? _callIncomingSub;
|
||||
StreamSubscription<String>? _serverErrorSub;
|
||||
StreamSubscription<AccountNotice>? _accountNoticeSub;
|
||||
Timer? _scheduleTimer;
|
||||
String? _lastVpnNotice;
|
||||
DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
@@ -477,6 +478,18 @@ class KometAppState extends State<KometApp>
|
||||
showCustomNotificationOnOverlay(overlay, msg);
|
||||
}
|
||||
});
|
||||
|
||||
_accountNoticeSub = accountModule.noticeStream.listen((notice) {
|
||||
final overlay = KometApp.navigatorKey.currentState?.overlay;
|
||||
final ctx = KometApp.navigatorKey.currentContext;
|
||||
if (overlay == null || ctx == null || !ctx.mounted) return;
|
||||
final l10n = AppLocalizations.of(ctx);
|
||||
if (l10n == null) return;
|
||||
final message = switch (notice) {
|
||||
AccountNotice.resurrectingProfile => l10n.profileResurrecting,
|
||||
};
|
||||
showCustomNotificationOnOverlay(overlay, message);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ensureFullScreenIntentPermission() async {
|
||||
@@ -534,6 +547,7 @@ class KometAppState extends State<KometApp>
|
||||
_vpnBypassSub?.cancel();
|
||||
_callIncomingSub?.cancel();
|
||||
_serverErrorSub?.cancel();
|
||||
_accountNoticeSub?.cancel();
|
||||
_scheduleTimer?.cancel();
|
||||
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
||||
AppAmoled.current.removeListener(_onAmoledChanged);
|
||||
|
||||
Reference in New Issue
Block a user