From 2a5c9d0a62bad103220f196670dd5aa46a323831 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 31 Dec 2023 10:03:07 +0100 Subject: [PATCH] feat: Backup session and restore on database error --- assets/l10n/intl_en.arb | 21 ++- lib/utils/client_manager.dart | 27 ++-- lib/utils/init_with_restore.dart | 139 ++++++++++++++++++ .../flutter_matrix_sdk_database_builder.dart | 5 +- lib/widgets/matrix.dart | 4 + 5 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 lib/utils/init_with_restore.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 42dc84bb..539ac47f 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2386,11 +2386,28 @@ "decline": "Decline", "thisDevice": "This device:", "initAppError": "An error occured while init the app", - "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}", + "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", "@databaseBuildErrorBody": { "type": "text", "placeholders": { - "url": {} + "url": {}, + "error": {} + } + }, + "sessionLostBody": "Your session is lost. Please report this error to the developers at {url}. The error message is: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "restoreSessionBody": "The app now tries to restore your session from the backup. Please report this error to the developers at {url}. The error message is: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} } } } \ No newline at end of file diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 01bf0252..2d171336 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -15,6 +15,7 @@ import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; +import 'package:fluffychat/utils/init_with_restore.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart'; @@ -46,21 +47,17 @@ abstract class ClientManager { if (initialize) { await Future.wait( clients.map( - (client) => client - .init( - waitForFirstSync: false, - waitUntilLoadCompletedLoaded: false, - onMigration: () { - final l10n = lookupL10n(PlatformDispatcher.instance.locale); - sendInitNotification( - l10n.databaseMigrationTitle, - l10n.databaseMigrationBody, - ); - }, - ) - .catchError( - (e, s) => Logs().e('Unable to initialize client', e, s), - ), + (client) => client.initWithRestore( + onMigration: () { + final l10n = lookupL10n(PlatformDispatcher.instance.locale); + sendInitNotification( + l10n.databaseMigrationTitle, + l10n.databaseMigrationBody, + ); + }, + ).catchError( + (e, s) => Logs().e('Unable to initialize client', e, s), + ), ), ); } diff --git a/lib/utils/init_with_restore.dart b/lib/utils/init_with_restore.dart new file mode 100644 index 00000000..04ed47db --- /dev/null +++ b/lib/utils/init_with_restore.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; + +class SessionBackup { + final String? olmAccount; + final String accessToken; + final String userId; + final String homeserver; + final String? deviceId; + final String? deviceName; + + const SessionBackup({ + required this.olmAccount, + required this.accessToken, + required this.userId, + required this.homeserver, + required this.deviceId, + this.deviceName, + }); + + factory SessionBackup.fromJsonString(String json) => + SessionBackup.fromJson(jsonDecode(json)); + + factory SessionBackup.fromJson(Map json) => SessionBackup( + olmAccount: json['olm_account'], + accessToken: json['access_token'], + userId: json['user_id'], + homeserver: json['homeserver'], + deviceId: json['device_id'], + deviceName: json['device_name'], + ); + + Map toJson() => { + 'olm_account': olmAccount, + 'access_token': accessToken, + 'user_id': userId, + 'homeserver': homeserver, + 'device_id': deviceId, + if (deviceName != null) 'device_name': deviceName, + }; + + @override + String toString() => jsonEncode(toJson()); +} + +extension InitWithRestoreExtension on Client { + static Future deleteSessionBackup(String clientName) async { + final storage = PlatformInfos.isMobile || PlatformInfos.isLinux + ? const FlutterSecureStorage() + : null; + await storage?.delete( + key: '${AppConfig.applicationName}_session_backup_$clientName', + ); + } + + Future initWithRestore({void Function()? onMigration}) async { + final storageKey = + '${AppConfig.applicationName}_session_backup_$clientName'; + final storage = PlatformInfos.isMobile || PlatformInfos.isLinux + ? const FlutterSecureStorage() + : null; + + try { + await init( + onMigration: onMigration, + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + ); + if (isLogged()) { + final accessToken = this.accessToken; + final homeserver = this.homeserver?.toString(); + final deviceId = deviceID; + final userId = userID; + final hasBackup = accessToken != null && + homeserver != null && + deviceId != null && + userId != null; + assert(hasBackup); + if (hasBackup) { + Logs().v('Store session in backup'); + storage?.write( + key: storageKey, + value: SessionBackup( + olmAccount: encryption?.pickledOlmAccount, + accessToken: accessToken, + deviceId: deviceId, + homeserver: homeserver, + deviceName: deviceName, + userId: userId, + ).toString(), + ); + } + } + } catch (e) { + final l10n = lookupL10n(PlatformDispatcher.instance.locale); + final sessionBackupString = await storage?.read(key: storageKey); + if (sessionBackupString == null) { + ClientManager.sendInitNotification( + l10n.initAppError, + l10n.sessionLostBody(AppConfig.newIssueUrl.toString(), e.toString()), + ); + rethrow; + } + + ClientManager.sendInitNotification( + l10n.initAppError, + l10n.restoreSessionBody(AppConfig.newIssueUrl.toString(), e.toString()), + ); + try { + final sessionBackup = SessionBackup.fromJsonString(sessionBackupString); + await init( + newToken: sessionBackup.accessToken, + newOlmAccount: sessionBackup.olmAccount, + newDeviceID: sessionBackup.deviceId, + newDeviceName: sessionBackup.deviceName, + newHomeserver: Uri.tryParse(sessionBackup.homeserver), + newUserID: sessionBackup.userId, + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + onMigration: onMigration, + ); + } catch (e) { + ClientManager.sendInitNotification( + l10n.initAppError, + l10n.sessionLostBody(AppConfig.newIssueUrl.toString(), e.toString()), + ); + rethrow; + } + } + } +} diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart index 850b8ebf..3cc80e82 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_sdk_database_builder.dart @@ -37,7 +37,10 @@ Future flutterMatrixSdkDatabaseBuilder(Client client) async { final l10n = lookupL10n(PlatformDispatcher.instance.locale); ClientManager.sendInitNotification( l10n.initAppError, - l10n.databaseBuildErrorBody(AppConfig.newIssueUrl.toString()), + l10n.databaseBuildErrorBody( + AppConfig.newIssueUrl.toString(), + e.toString(), + ), ); return FlutterHiveCollectionsDatabase.databaseBuilder(client); diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index a18835ee..d6d526c3 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -19,6 +19,7 @@ import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/init_with_restore.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; @@ -314,6 +315,9 @@ class MatrixState extends State with WidgetsBindingObserver { }); onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { final loggedInWithMultipleClients = widget.clients.length > 1; + if (state == LoginState.loggedOut) { + InitWithRestoreExtension.deleteSessionBackup(name); + } if (loggedInWithMultipleClients && state != LoginState.loggedIn) { _cancelSubs(c.clientName); widget.clients.remove(c);