Merge pull request #759 from krille-chan/krille/restore-session

feat: Backup session and restore on database error
This commit is contained in:
Krille-chan 2023-12-31 10:29:28 +01:00 committed by GitHub
commit dad8576a23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 18 deletions

View file

@ -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": {}
}
}
}

View file

@ -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),
),
),
);
}

View file

@ -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<String, dynamic> 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<String, dynamic> 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<void> deleteSessionBackup(String clientName) async {
final storage = PlatformInfos.isMobile || PlatformInfos.isLinux
? const FlutterSecureStorage()
: null;
await storage?.delete(
key: '${AppConfig.applicationName}_session_backup_$clientName',
);
}
Future<void> 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;
}
}
}
}

View file

@ -37,7 +37,10 @@ Future<DatabaseApi> 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);

View file

@ -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<Matrix> 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);