diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 61e73f0..a2e8177 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,24 +1,50 @@ +import "dart:convert"; + import "package:flutter/foundation.dart"; import "package:matrix/matrix.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/secure_storage_controller.dart"; +import "package:nexus/models/session_backup.dart"; import "package:path/path.dart"; import "package:path_provider/path_provider.dart"; import "package:sqflite_common_ffi/sqflite_ffi.dart"; class ClientController extends AsyncNotifier { + static const sessionBackupKey = "sessionBackup"; @override - Future build() async => Client( - "nexus", - logLevel: kReleaseMode ? Level.warning : Level.verbose, - importantStateEvents: {"im.ponies.room_emotes"}, - supportedLoginTypes: {AuthenticationTypes.password}, - database: await MatrixSdkDatabase.init( + Future build() async { + final client = Client( "nexus", - database: await databaseFactoryFfi.openDatabase( - join((await getApplicationSupportDirectory()).path, "database.db"), + logLevel: kReleaseMode ? Level.warning : Level.verbose, + importantStateEvents: {"im.ponies.room_emotes"}, + supportedLoginTypes: {AuthenticationTypes.password}, + database: await MatrixSdkDatabase.init( + "nexus", + database: await databaseFactoryFfi.openDatabase( + join((await getApplicationSupportDirectory()).path, "database.db"), + ), ), - ), - ); + ); + + final backupJson = await ref + .watch(SecureStorageController.provider.notifier) + .get(sessionBackupKey); + + if (backupJson != null) { + final backup = SessionBackup.fromJson(json.decode(backupJson)); + + await client.init( + waitForFirstSync: false, + newToken: backup.accessToken, + newHomeserver: backup.homeserver, + newUserID: backup.userID, + newDeviceID: backup.deviceID, + newDeviceName: backup.deviceName, + ); + } + + return client; + } Future setHomeserver(Uri homeserverUrl) async { final client = await future; @@ -33,13 +59,28 @@ class ClientController extends AsyncNotifier { Future login(String username, String password) async { final client = await future; try { - await client.login( + final deviceName = + "Nexus Client login at ${DateTime.now().toIso8601String()}"; + final details = await MatrixApi(homeserver: client.homeserver).login( LoginType.mLoginPassword, - initialDeviceDisplayName: - "Nexus Client login at ${DateTime.now().toIso8601String()}", + initialDeviceDisplayName: deviceName, identifier: AuthenticationUserIdentifier(user: username), password: password, ); + await ref + .watch(SecureStorageController.provider.notifier) + .set( + sessionBackupKey, + json.encode( + SessionBackup( + accessToken: details.accessToken, + homeserver: client.homeserver!, + userID: details.userId, + deviceID: details.deviceId, + deviceName: deviceName, + ).toJson(), + ), + ); ref.invalidateSelf(); return true; } catch (_) { diff --git a/lib/controllers/secure_storage_controller.dart b/lib/controllers/secure_storage_controller.dart index daac694..8a579f5 100644 --- a/lib/controllers/secure_storage_controller.dart +++ b/lib/controllers/secure_storage_controller.dart @@ -1,4 +1,3 @@ -import "package:matrix/matrix.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:simple_secure_storage/simple_secure_storage.dart"; @@ -6,6 +5,21 @@ class SecureStorageController extends AsyncNotifier { @override Future build() => SimpleSecureStorage.initialize(); + Future get(String key) async { + await future; + return SimpleSecureStorage.read(key); + } + + Future set(String key, String value) async { + await future; + return SimpleSecureStorage.write(key, value); + } + + Future clear() async { + await future; + return SimpleSecureStorage.clear(); + } + static final provider = AsyncNotifierProvider( SecureStorageController.new, ); diff --git a/lib/models/session_backup.dart b/lib/models/session_backup.dart new file mode 100644 index 0000000..0245c7e --- /dev/null +++ b/lib/models/session_backup.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "session_backup.freezed.dart"; +part "session_backup.g.dart"; + +@freezed +abstract class SessionBackup with _$SessionBackup { + const factory SessionBackup({ + required String accessToken, + required Uri homeserver, + required String userID, + required String deviceID, + required String deviceName, + }) = _SessionBackup; + + factory SessionBackup.fromJson(Map json) => + _$SessionBackupFromJson(json); +} diff --git a/lib/models/session_backup.freezed.dart b/lib/models/session_backup.freezed.dart new file mode 100644 index 0000000..a25d0fb --- /dev/null +++ b/lib/models/session_backup.freezed.dart @@ -0,0 +1,289 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'session_backup.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$SessionBackup { + + String get accessToken; Uri get homeserver; String get userID; String get deviceID; String get deviceName; +/// Create a copy of SessionBackup +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SessionBackupCopyWith get copyWith => _$SessionBackupCopyWithImpl(this as SessionBackup, _$identity); + + /// Serializes this SessionBackup to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SessionBackup&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.homeserver, homeserver) || other.homeserver == homeserver)&&(identical(other.userID, userID) || other.userID == userID)&&(identical(other.deviceID, deviceID) || other.deviceID == deviceID)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,accessToken,homeserver,userID,deviceID,deviceName); + +@override +String toString() { + return 'SessionBackup(accessToken: $accessToken, homeserver: $homeserver, userID: $userID, deviceID: $deviceID, deviceName: $deviceName)'; +} + + +} + +/// @nodoc +abstract mixin class $SessionBackupCopyWith<$Res> { + factory $SessionBackupCopyWith(SessionBackup value, $Res Function(SessionBackup) _then) = _$SessionBackupCopyWithImpl; +@useResult +$Res call({ + String accessToken, Uri homeserver, String userID, String deviceID, String deviceName +}); + + + + +} +/// @nodoc +class _$SessionBackupCopyWithImpl<$Res> + implements $SessionBackupCopyWith<$Res> { + _$SessionBackupCopyWithImpl(this._self, this._then); + + final SessionBackup _self; + final $Res Function(SessionBackup) _then; + +/// Create a copy of SessionBackup +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? accessToken = null,Object? homeserver = null,Object? userID = null,Object? deviceID = null,Object? deviceName = null,}) { + return _then(_self.copyWith( +accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable +as String,homeserver: null == homeserver ? _self.homeserver : homeserver // ignore: cast_nullable_to_non_nullable +as Uri,userID: null == userID ? _self.userID : userID // ignore: cast_nullable_to_non_nullable +as String,deviceID: null == deviceID ? _self.deviceID : deviceID // ignore: cast_nullable_to_non_nullable +as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SessionBackup]. +extension SessionBackupPatterns on SessionBackup { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SessionBackup value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SessionBackup() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SessionBackup value) $default,){ +final _that = this; +switch (_that) { +case _SessionBackup(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SessionBackup value)? $default,){ +final _that = this; +switch (_that) { +case _SessionBackup() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String accessToken, Uri homeserver, String userID, String deviceID, String deviceName)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SessionBackup() when $default != null: +return $default(_that.accessToken,_that.homeserver,_that.userID,_that.deviceID,_that.deviceName);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String accessToken, Uri homeserver, String userID, String deviceID, String deviceName) $default,) {final _that = this; +switch (_that) { +case _SessionBackup(): +return $default(_that.accessToken,_that.homeserver,_that.userID,_that.deviceID,_that.deviceName);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String accessToken, Uri homeserver, String userID, String deviceID, String deviceName)? $default,) {final _that = this; +switch (_that) { +case _SessionBackup() when $default != null: +return $default(_that.accessToken,_that.homeserver,_that.userID,_that.deviceID,_that.deviceName);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SessionBackup implements SessionBackup { + const _SessionBackup({required this.accessToken, required this.homeserver, required this.userID, required this.deviceID, required this.deviceName}); + factory _SessionBackup.fromJson(Map json) => _$SessionBackupFromJson(json); + +@override final String accessToken; +@override final Uri homeserver; +@override final String userID; +@override final String deviceID; +@override final String deviceName; + +/// Create a copy of SessionBackup +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SessionBackupCopyWith<_SessionBackup> get copyWith => __$SessionBackupCopyWithImpl<_SessionBackup>(this, _$identity); + +@override +Map toJson() { + return _$SessionBackupToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SessionBackup&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.homeserver, homeserver) || other.homeserver == homeserver)&&(identical(other.userID, userID) || other.userID == userID)&&(identical(other.deviceID, deviceID) || other.deviceID == deviceID)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,accessToken,homeserver,userID,deviceID,deviceName); + +@override +String toString() { + return 'SessionBackup(accessToken: $accessToken, homeserver: $homeserver, userID: $userID, deviceID: $deviceID, deviceName: $deviceName)'; +} + + +} + +/// @nodoc +abstract mixin class _$SessionBackupCopyWith<$Res> implements $SessionBackupCopyWith<$Res> { + factory _$SessionBackupCopyWith(_SessionBackup value, $Res Function(_SessionBackup) _then) = __$SessionBackupCopyWithImpl; +@override @useResult +$Res call({ + String accessToken, Uri homeserver, String userID, String deviceID, String deviceName +}); + + + + +} +/// @nodoc +class __$SessionBackupCopyWithImpl<$Res> + implements _$SessionBackupCopyWith<$Res> { + __$SessionBackupCopyWithImpl(this._self, this._then); + + final _SessionBackup _self; + final $Res Function(_SessionBackup) _then; + +/// Create a copy of SessionBackup +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? accessToken = null,Object? homeserver = null,Object? userID = null,Object? deviceID = null,Object? deviceName = null,}) { + return _then(_SessionBackup( +accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable +as String,homeserver: null == homeserver ? _self.homeserver : homeserver // ignore: cast_nullable_to_non_nullable +as Uri,userID: null == userID ? _self.userID : userID // ignore: cast_nullable_to_non_nullable +as String,deviceID: null == deviceID ? _self.deviceID : deviceID // ignore: cast_nullable_to_non_nullable +as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/pubspec.lock b/pubspec.lock index 418efc2..1e56f04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -808,13 +808,21 @@ packages: source: hosted version: "0.7.2" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe" + url: "https://pub.dev" + source: hosted + version: "6.11.1" just_throttle_it: dependency: transitive description: @@ -1292,6 +1300,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + url: "https://pub.dev" + source: hosted + version: "1.3.8" source_map_stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 38830a5..6fad88a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: flutter_widget_from_html_core: ^0.17.0 flutter_svg: ^2.2.2 simple_secure_storage: ^0.3.6 + json_annotation: ^4.9.0 dev_dependencies: build_runner: ^2.4.11 @@ -66,6 +67,7 @@ dev_dependencies: freezed: ^3.2.3 riverpod_lint: ^3.0.3 flutter_launcher_icons: ^0.14.1 + json_serializable: ^6.11.1 flutter_launcher_icons: ios: true