From 6e8ba551ad101b69fb50413bd209669d37c75898 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 18 Jan 2024 19:44:15 -0500 Subject: [PATCH] more refactor work --- .../cubit/account_record_cubit.dart | 16 ++ lib/account_manager/cubit/cubit.dart | 1 + lib/account_manager/models/account_info.dart | 7 +- .../account_repository.dart | 107 ++++++------- .../valid_contact_invitation.dart | 141 +++++++++++++++++ lib/layout/home.dart | 7 +- lib/layout/main_pager/account.dart | 1 + .../contact_invitation_list_manager.dart | 142 ------------------ .../lib/dht_support/src/dht_short_array.dart | 2 + .../src/dht_short_array_cubit.dart | 84 +++++++++++ 10 files changed, 295 insertions(+), 213 deletions(-) create mode 100644 lib/account_manager/cubit/account_record_cubit.dart create mode 100644 lib/contact_invitation/valid_contact_invitation.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart diff --git a/lib/account_manager/cubit/account_record_cubit.dart b/lib/account_manager/cubit/account_record_cubit.dart new file mode 100644 index 0000000..65306dd --- /dev/null +++ b/lib/account_manager/cubit/account_record_cubit.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:veilid_support/veilid_support.dart'; + +import '../../proto/proto.dart' as proto; + +class AccountRecordCubit extends DefaultDHTRecordCubit { + AccountRecordCubit({ + required super.record, + }) : super(decodeState: proto.Account.fromBuffer); + + @override + Future close() async { + await super.close(); + } +} diff --git a/lib/account_manager/cubit/cubit.dart b/lib/account_manager/cubit/cubit.dart index 0f56c84..d86274b 100644 --- a/lib/account_manager/cubit/cubit.dart +++ b/lib/account_manager/cubit/cubit.dart @@ -1,3 +1,4 @@ +export 'account_record_cubit.dart'; export 'active_user_login_cubit/active_user_login_cubit.dart'; export 'local_accounts_cubit/local_accounts_cubit.dart'; export 'user_logins_cubit/user_logins_cubit.dart'; diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 14b25dc..7f2e058 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; -import 'package:veilid_support/veilid_support.dart'; + +import 'active_account_info.dart'; enum AccountInfoStatus { noAccount, @@ -13,10 +14,10 @@ class AccountInfo { const AccountInfo({ required this.status, required this.active, - this.accountRecord, + required this.activeAccountInfo, }); final AccountInfoStatus status; final bool active; - final DHTRecord? accountRecord; + final ActiveAccountInfo? activeAccountInfo; } diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 3132923..60d493e 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -82,66 +82,40 @@ class AccountRepository { return userLogins[idx]; } - AccountInfo getAccountInfo({required TypedKey accountMasterRecordKey}) { + AccountInfo? getAccountInfo({TypedKey? accountMasterRecordKey}) { + // Get active user if we have one + if (accountMasterRecordKey == null) { + final activeUserLogin = getActiveUserLogin(); + if (activeUserLogin == null) { + // No user logged in + return null; + } + accountMasterRecordKey = activeUserLogin; + } + // Get which local account we want to fetch the profile for final localAccount = fetchLocalAccount(accountMasterRecordKey: accountMasterRecordKey); if (localAccount == null) { // Local account does not exist return const AccountInfo( - status: AccountInfoStatus.noAccount, active: false); + status: AccountInfoStatus.noAccount, + active: false, + activeAccountInfo: null); } // See if we've logged into this account or if it is locked final activeUserLogin = getActiveUserLogin(); final active = activeUserLogin == accountMasterRecordKey; - final login = + final userLogin = fetchUserLogin(accountMasterRecordKey: accountMasterRecordKey); - if (login == null) { - // Account was locked - return AccountInfo( - status: AccountInfoStatus.accountLocked, active: active); - } - - // Pull the account DHT key, decode it and return it - final pool = DHTRecordPool.instance; - final accountRecord = - pool.getOpenedRecord(login.accountRecordInfo.accountRecord.recordKey); - if (accountRecord == null) { - // Account could not be read or decrypted from DHT - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - - // Got account, decrypted and decoded - return AccountInfo( - status: AccountInfoStatus.accountReady, - active: active, - accountRecord: accountRecord); - } - - Future fetchActiveAccountInfo() async { - // See if we've logged into this account or if it is locked - final activeUserLogin = getActiveUserLogin(); - if (activeUserLogin == null) { - // No user logged in - return null; - } - - // Get the user login - final userLogin = fetchUserLogin(accountMasterRecordKey: activeUserLogin); if (userLogin == null) { // Account was locked - return null; - } - - // Get which local account we want to fetch the profile for - final localAccount = - fetchLocalAccount(accountMasterRecordKey: activeUserLogin); - if (localAccount == null) { - // Local account does not exist - return null; + return AccountInfo( + status: AccountInfoStatus.accountLocked, + active: active, + activeAccountInfo: null); } // Pull the account DHT key, decode it and return it @@ -149,14 +123,21 @@ class AccountRepository { final accountRecord = pool .getOpenedRecord(userLogin.accountRecordInfo.accountRecord.recordKey); if (accountRecord == null) { - return null; + // Account could not be read or decrypted from DHT + return AccountInfo( + status: AccountInfoStatus.accountInvalid, + active: active, + activeAccountInfo: null); } // Got account, decrypted and decoded - return ActiveAccountInfo( - localAccount: localAccount, - userLogin: userLogin, - accountRecord: accountRecord, + return AccountInfo( + status: AccountInfoStatus.accountReady, + active: active, + activeAccountInfo: ActiveAccountInfo( + localAccount: localAccount, + userLogin: userLogin, + accountRecord: accountRecord), ); } @@ -411,24 +392,21 @@ class AccountRepository { // For all user logins if they arent open yet final activeLogins = await _activeLogins.get(); for (final userLogin in activeLogins.userLogins) { + //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; final existingAccountRecord = pool.getOpenedRecord(accountRecordKey); - if (existingAccountRecord != null) { - continue; + if (existingAccountRecord == null) { + final localAccount = fetchLocalAccount( + accountMasterRecordKey: userLogin.accountMasterRecordKey); + + // Record not yet open, do it + final record = await pool.openOwned( + userLogin.accountRecordInfo.accountRecord, + parent: localAccount!.identityMaster.identityRecordKey); + // Watch the record's only (default) key + await record.watch(); } - final localAccount = fetchLocalAccount( - accountMasterRecordKey: userLogin.accountMasterRecordKey); - - // Record not yet open, do it - final record = await pool.openOwned( - userLogin.accountRecordInfo.accountRecord, - parent: localAccount!.identityMaster.identityRecordKey); - // Watch the record's only (default) key - await record.watch(); - - // .scope( - // (accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer)); } } @@ -437,6 +415,7 @@ class AccountRepository { final activeLogins = await _activeLogins.get(); for (final userLogin in activeLogins.userLogins) { + //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; final accountRecord = pool.getOpenedRecord(accountRecordKey); diff --git a/lib/contact_invitation/valid_contact_invitation.dart b/lib/contact_invitation/valid_contact_invitation.dart new file mode 100644 index 0000000..ec21444 --- /dev/null +++ b/lib/contact_invitation/valid_contact_invitation.dart @@ -0,0 +1,141 @@ +////////////////////////////////////////////////// +/// + +class ValidContactInvitation { + ValidContactInvitation._( + {required ContactInvitationListManager contactInvitationManager, + required proto.SignedContactInvitation signedContactInvitation, + required proto.ContactInvitation contactInvitation, + required TypedKey contactRequestInboxKey, + required proto.ContactRequest contactRequest, + required proto.ContactRequestPrivate contactRequestPrivate, + required IdentityMaster contactIdentityMaster, + required KeyPair writer}) + : _contactInvitationManager = contactInvitationManager, + _signedContactInvitation = signedContactInvitation, + _contactInvitation = contactInvitation, + _contactRequestInboxKey = contactRequestInboxKey, + _contactRequest = contactRequest, + _contactRequestPrivate = contactRequestPrivate, + _contactIdentityMaster = contactIdentityMaster, + _writer = writer; + + Future accept() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + try { + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + // ignore: prefer_expression_function_bodies + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + // Create local conversation key for this + // contact and send via contact response + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + _contactIdentityMaster.identityPublicTypedKey(), + callback: (localConversation) async { + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversation.key.toProto() + ..identityMasterRecordKey = activeAccountInfo + .localAccount.identityMaster.masterRecordKey + .toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); + + final cs = await pool.veilid + .getCryptoSystem(_contactRequestInboxKey.kind); + + final identitySignature = await cs.sign( + activeAccountInfo + .localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); + + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the acceptance to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, + signedContactResponse, + subkey: 1) != + null) { + throw Exception('failed to accept contact invitation'); + } + return AcceptedContact( + profile: _contactRequestPrivate.profile, + remoteIdentity: _contactIdentityMaster, + remoteConversationRecordKey: proto.TypedKeyProto.fromProto( + _contactRequestPrivate.chatRecordKey), + localConversationRecordKey: localConversation.key, + ); + }); + }); + } on Exception catch (e) { + log.debug('exception: $e', e); + return null; + } + } + + Future reject() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + final cs = + await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); + + final contactResponse = proto.ContactResponse() + ..accept = false + ..identityMasterRecordKey = activeAccountInfo + .localAccount.identityMaster.masterRecordKey + .toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); + + final identitySignature = await cs.sign( + activeAccountInfo.localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); + + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the rejection to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, signedContactResponse, + subkey: 1) != + null) { + log.error('failed to reject contact invitation'); + return false; + } + return true; + }); + } + + // + ContactInvitationListManager _contactInvitationManager; + proto.SignedContactInvitation _signedContactInvitation; + proto.ContactInvitation _contactInvitation; + TypedKey _contactRequestInboxKey; + proto.ContactRequest _contactRequest; + proto.ContactRequestPrivate _contactRequestPrivate; + IdentityMaster _contactIdentityMaster; + KeyPair _writer; +} diff --git a/lib/layout/home.dart b/lib/layout/home.dart index eeec9dc..438ec6a 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home.dart @@ -67,8 +67,7 @@ class HomePageState extends State with TickerProviderStateMixin { final scale = theme.extension()!; return BlocProvider( - create: (context) => DefaultDHTRecordCubit( - record: accountRecord, decodeState: proto.Account.fromBuffer), + create: (context) => AccountRecordCubit(record: accountRecord), child: Column(children: [ Row(children: [ IconButton( @@ -87,7 +86,7 @@ class HomePageState extends State with TickerProviderStateMixin { context.go('/home/settings'); }).paddingLTRB(0, 0, 8, 0), context - .watch>() + .watch() .state .builder((context, account) => ProfileWidget( name: account.profile.name, @@ -96,7 +95,7 @@ class HomePageState extends State with TickerProviderStateMixin { .expanded(), ]).paddingAll(8), context - .watch>() + .watch() .state .builder((context, account) => MainPager( localAccounts: localAccounts, diff --git a/lib/layout/main_pager/account.dart b/lib/layout/main_pager/account.dart index ece96c1..3f25171 100644 --- a/lib/layout/main_pager/account.dart +++ b/lib/layout/main_pager/account.dart @@ -11,6 +11,7 @@ import '../../../proto/proto.dart' as proto; import '../../account_manager/account_manager.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../contacts/contacts.dart'; +import '../../theme/theme.dart'; class AccountPage extends StatefulWidget { const AccountPage({ diff --git a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart index 550169e..04cb273 100644 --- a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart +++ b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart @@ -439,145 +439,3 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { final DHTShortArray _dhtRecord; IList _records; } - -////////////////////////////////////////////////// -/// - -class ValidContactInvitation { - ValidContactInvitation._( - {required ContactInvitationListManager contactInvitationManager, - required proto.SignedContactInvitation signedContactInvitation, - required proto.ContactInvitation contactInvitation, - required TypedKey contactRequestInboxKey, - required proto.ContactRequest contactRequest, - required proto.ContactRequestPrivate contactRequestPrivate, - required IdentityMaster contactIdentityMaster, - required KeyPair writer}) - : _contactInvitationManager = contactInvitationManager, - _signedContactInvitation = signedContactInvitation, - _contactInvitation = contactInvitation, - _contactRequestInboxKey = contactRequestInboxKey, - _contactRequest = contactRequest, - _contactRequestPrivate = contactRequestPrivate, - _contactIdentityMaster = contactIdentityMaster, - _writer = writer; - - Future accept() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; - try { - // Ensure we don't delete this if we're trying to chat to self - final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(_contactRequestInboxKey, _writer, - parent: accountRecordKey)) - // ignore: prefer_expression_function_bodies - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - // Create local conversation key for this - // contact and send via contact response - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - _contactIdentityMaster.identityPublicTypedKey(), - callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final cs = await pool.veilid - .getCryptoSystem(_contactRequestInboxKey.kind); - - final identitySignature = await cs.sign( - activeAccountInfo - .localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, - signedContactResponse, - subkey: 1) != - null) { - throw Exception('failed to accept contact invitation'); - } - return AcceptedContact( - profile: _contactRequestPrivate.profile, - remoteIdentity: _contactIdentityMaster, - remoteConversationRecordKey: proto.TypedKeyProto.fromProto( - _contactRequestPrivate.chatRecordKey), - localConversationRecordKey: localConversation.key, - ); - }); - }); - } on Exception catch (e) { - log.debug('exception: $e', e); - return null; - } - } - - Future reject() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; - - // Ensure we don't delete this if we're trying to chat to self - final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(_contactRequestInboxKey, _writer, - parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - final cs = - await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); - - final contactResponse = proto.ContactResponse() - ..accept = false - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the rejection to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to reject contact invitation'); - return false; - } - return true; - }); - } - - // - ContactInvitationListManager _contactInvitationManager; - proto.SignedContactInvitation _signedContactInvitation; - proto.ContactInvitation _contactInvitation; - TypedKey _contactRequestInboxKey; - proto.ContactRequest _contactRequest; - proto.ContactRequestPrivate _contactRequestPrivate; - IdentityMaster _contactIdentityMaster; - KeyPair _writer; -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index ed917d1..08e2e95 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -29,6 +29,8 @@ class _DHTShortArrayCache { } } +xxxx add listening to head and linked records + class DHTShortArray { DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord, diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart new file mode 100644 index 0000000..3484b0f --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../../veilid_support.dart'; + +class DHTShortArrayCubit extends Cubit>> { + DHTShortArrayCubit({ + required DHTShortArray shortArray, + required T Function(List data) decodeElement, + }) : super(const AsyncValue.loading()) { + Future.delayed(Duration.zero, () async { + // Make initial state update + try { + final initialState = await initialStateFunction(record); + if (initialState != null) { + emit(AsyncValue.data(initialState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + + shortArray. xxx add listen to head and linked records in dht_short_array + + _subscription = await record.listen((update) async { + try { + final newState = + await stateFunction(record, update.subkeys, update.valueData); + if (newState != null) { + emit(AsyncValue.data(newState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + }); + }); + } + + @override + Future close() async { + await _subscription?.cancel(); + _subscription = null; + await super.close(); + } + + StreamSubscription? _subscription; +} + +// Cubit that watches the default subkey value of a dhtrecord +class DefaultDHTRecordCubit extends DHTRecordCubit { + DefaultDHTRecordCubit({ + required super.record, + required T Function(List data) decodeState, + }) : super( + initialStateFunction: (record) async { + final initialData = await record.get(); + if (initialData == null) { + return null; + } + return decodeState(initialData); + }, + stateFunction: (record, subkeys, valueData) async { + final defaultSubkey = record.subkeyOrDefault(-1); + if (subkeys.containsSubkey(defaultSubkey)) { + final Uint8List data; + final firstSubkey = subkeys.firstOrNull!.low; + if (firstSubkey != defaultSubkey) { + final maybeData = await record.get(forceRefresh: true); + if (maybeData == null) { + return null; + } + data = maybeData; + } else { + data = valueData.data; + } + final newState = decodeState(data); + return newState; + } + return null; + }, + ); +}