From e8980743875104b8fd0313ca5380f4aa8407e78d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 21 Dec 2023 12:10:54 -0500 Subject: [PATCH] refactoring --- lib/managers/contact_list_manager.dart | 0 lib/managers/valid_contact_invitation.dart | 1 + lib/providers/account.dart | 38 +- lib/providers/account.g.dart | 91 +-- lib/providers/chat.g.dart | 7 +- .../contact_invitation_list_manager.dart | 583 ++++++++++++++++++ .../contact_invitation_list_manager.g.dart | 202 ++++++ lib/providers/contact_invite.dart | 503 --------------- lib/providers/contact_invite.g.dart | 6 +- lib/providers/conversation.dart | 483 +++++++-------- lib/providers/conversation.g.dart | 6 +- lib/providers/local_accounts.dart | 2 +- 12 files changed, 1100 insertions(+), 822 deletions(-) create mode 100644 lib/managers/contact_list_manager.dart create mode 100644 lib/managers/valid_contact_invitation.dart create mode 100644 lib/providers/contact_invitation_list_manager.dart create mode 100644 lib/providers/contact_invitation_list_manager.g.dart diff --git a/lib/managers/contact_list_manager.dart b/lib/managers/contact_list_manager.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/managers/valid_contact_invitation.dart b/lib/managers/valid_contact_invitation.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/managers/valid_contact_invitation.dart @@ -0,0 +1 @@ + diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 22e532f..93f1ce9 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -1,10 +1,10 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/local_account.dart'; -import '../proto/proto.dart' as proto; import '../entities/user_login.dart'; +import '../proto/proto.dart' as proto; import '../veilid_support/veilid_support.dart'; - import 'local_accounts.dart'; import 'logins.dart'; @@ -17,22 +17,23 @@ enum AccountInfoStatus { accountReady, } +@immutable class AccountInfo { - AccountInfo({ + const AccountInfo({ required this.status, required this.active, this.account, }); - AccountInfoStatus status; - bool active; - proto.Account? account; + final AccountInfoStatus status; + final bool active; + final proto.Account? account; } /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents @riverpod -Future fetchAccount(FetchAccountRef ref, +Future fetchAccountInfo(FetchAccountInfoRef ref, {required TypedKey accountMasterRecordKey}) async { // Get which local account we want to fetch the profile for final localAccount = await ref.watch( @@ -40,7 +41,8 @@ Future fetchAccount(FetchAccountRef ref, .future); if (localAccount == null) { // Local account does not exist - return AccountInfo(status: AccountInfoStatus.noAccount, active: false); + return const AccountInfo( + status: AccountInfoStatus.noAccount, active: false); } // See if we've logged into this account or if it is locked @@ -74,21 +76,31 @@ Future fetchAccount(FetchAccountRef ref, status: AccountInfoStatus.accountReady, active: active, account: account); } +@immutable class ActiveAccountInfo { - ActiveAccountInfo({ + const ActiveAccountInfo({ required this.localAccount, required this.userLogin, required this.account, }); + // - LocalAccount localAccount; - UserLogin userLogin; - proto.Account account; + KeyPair getConversationWriter() { + final identityKey = localAccount.identityMaster.identityPublicKey; + final identitySecret = userLogin.identitySecret; + return KeyPair(key: identityKey, secret: identitySecret.value); + } + + // + final LocalAccount localAccount; + final UserLogin userLogin; + final proto.Account account; } /// Get the active account info @riverpod -Future fetchActiveAccount(FetchActiveAccountRef ref) async { +Future fetchActiveAccountInfo( + FetchActiveAccountInfoRef ref) async { // See if we've logged into this account or if it is locked final activeUserLogin = await ref.watch(loginsProvider.future .select((value) async => (await value).activeUserLogin)); diff --git a/lib/providers/account.g.dart b/lib/providers/account.g.dart index 6fba2b3..c1b1d59 100644 --- a/lib/providers/account.g.dart +++ b/lib/providers/account.g.dart @@ -6,7 +6,7 @@ part of 'account.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchAccountHash() => r'f3072fdd89611b53cd9821613acab450b3c08820'; +String _$fetchAccountInfoHash() => r'3d2e3b3ddce5158d03bceaf82cdb35bae000280c'; /// Copied from Dart SDK class _SystemHash { @@ -32,36 +32,36 @@ class _SystemHash { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -@ProviderFor(fetchAccount) -const fetchAccountProvider = FetchAccountFamily(); +/// Copied from [fetchAccountInfo]. +@ProviderFor(fetchAccountInfo) +const fetchAccountInfoProvider = FetchAccountInfoFamily(); /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -class FetchAccountFamily extends Family> { +/// Copied from [fetchAccountInfo]. +class FetchAccountInfoFamily extends Family> { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - const FetchAccountFamily(); + /// Copied from [fetchAccountInfo]. + const FetchAccountInfoFamily(); /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - FetchAccountProvider call({ + /// Copied from [fetchAccountInfo]. + FetchAccountInfoProvider call({ required Typed accountMasterRecordKey, }) { - return FetchAccountProvider( + return FetchAccountInfoProvider( accountMasterRecordKey: accountMasterRecordKey, ); } @override - FetchAccountProvider getProviderOverride( - covariant FetchAccountProvider provider, + FetchAccountInfoProvider getProviderOverride( + covariant FetchAccountInfoProvider provider, ) { return call( accountMasterRecordKey: provider.accountMasterRecordKey, @@ -80,38 +80,38 @@ class FetchAccountFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'fetchAccountProvider'; + String? get name => r'fetchAccountInfoProvider'; } /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -class FetchAccountProvider extends AutoDisposeFutureProvider { +/// Copied from [fetchAccountInfo]. +class FetchAccountInfoProvider extends AutoDisposeFutureProvider { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - FetchAccountProvider({ + /// Copied from [fetchAccountInfo]. + FetchAccountInfoProvider({ required Typed accountMasterRecordKey, }) : this._internal( - (ref) => fetchAccount( - ref as FetchAccountRef, + (ref) => fetchAccountInfo( + ref as FetchAccountInfoRef, accountMasterRecordKey: accountMasterRecordKey, ), - from: fetchAccountProvider, - name: r'fetchAccountProvider', + from: fetchAccountInfoProvider, + name: r'fetchAccountInfoProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$fetchAccountHash, - dependencies: FetchAccountFamily._dependencies, + : _$fetchAccountInfoHash, + dependencies: FetchAccountInfoFamily._dependencies, allTransitiveDependencies: - FetchAccountFamily._allTransitiveDependencies, + FetchAccountInfoFamily._allTransitiveDependencies, accountMasterRecordKey: accountMasterRecordKey, ); - FetchAccountProvider._internal( + FetchAccountInfoProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -125,12 +125,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { @override Override overrideWith( - FutureOr Function(FetchAccountRef provider) create, + FutureOr Function(FetchAccountInfoRef provider) create, ) { return ProviderOverride( origin: this, - override: FetchAccountProvider._internal( - (ref) => create(ref as FetchAccountRef), + override: FetchAccountInfoProvider._internal( + (ref) => create(ref as FetchAccountInfoRef), from: from, name: null, dependencies: null, @@ -143,12 +143,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { @override AutoDisposeFutureProviderElement createElement() { - return _FetchAccountProviderElement(this); + return _FetchAccountInfoProviderElement(this); } @override bool operator ==(Object other) { - return other is FetchAccountProvider && + return other is FetchAccountInfoProvider && other.accountMasterRecordKey == accountMasterRecordKey; } @@ -161,39 +161,40 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { } } -mixin FetchAccountRef on AutoDisposeFutureProviderRef { +mixin FetchAccountInfoRef on AutoDisposeFutureProviderRef { /// The parameter `accountMasterRecordKey` of this provider. Typed get accountMasterRecordKey; } -class _FetchAccountProviderElement - extends AutoDisposeFutureProviderElement with FetchAccountRef { - _FetchAccountProviderElement(super.provider); +class _FetchAccountInfoProviderElement + extends AutoDisposeFutureProviderElement + with FetchAccountInfoRef { + _FetchAccountInfoProviderElement(super.provider); @override Typed get accountMasterRecordKey => - (origin as FetchAccountProvider).accountMasterRecordKey; + (origin as FetchAccountInfoProvider).accountMasterRecordKey; } -String _$fetchActiveAccountHash() => - r'197e5dd793563ff1d9927309a5ec9db1c9f67f07'; +String _$fetchActiveAccountInfoHash() => + r'85276ff85b0e82c8d3c6313250954f5b578697d1'; /// Get the active account info /// -/// Copied from [fetchActiveAccount]. -@ProviderFor(fetchActiveAccount) -final fetchActiveAccountProvider = +/// Copied from [fetchActiveAccountInfo]. +@ProviderFor(fetchActiveAccountInfo) +final fetchActiveAccountInfoProvider = AutoDisposeFutureProvider.internal( - fetchActiveAccount, - name: r'fetchActiveAccountProvider', + fetchActiveAccountInfo, + name: r'fetchActiveAccountInfoProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$fetchActiveAccountHash, + : _$fetchActiveAccountInfoHash, dependencies: null, allTransitiveDependencies: null, ); -typedef FetchActiveAccountRef +typedef FetchActiveAccountInfoRef = AutoDisposeFutureProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/chat.g.dart b/lib/providers/chat.g.dart index 411eae1..3f2c8e8 100644 --- a/lib/providers/chat.g.dart +++ b/lib/providers/chat.g.dart @@ -6,13 +6,14 @@ part of 'chat.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchChatListHash() => r'407692f9d6794a5a2b356d7a34240624b211daa8'; +String _$fetchChatListHash() => r'0c166082625799862128dff09d9286f64785ba6c'; /// Get the active account contact list /// /// Copied from [fetchChatList]. @ProviderFor(fetchChatList) -final fetchChatListProvider = AutoDisposeFutureProvider?>.internal( +final fetchChatListProvider = + AutoDisposeFutureProvider?>.internal( fetchChatList, name: r'fetchChatListProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -22,6 +23,6 @@ final fetchChatListProvider = AutoDisposeFutureProvider?>.internal( allTransitiveDependencies: null, ); -typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; +typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invitation_list_manager.dart b/lib/providers/contact_invitation_list_manager.dart new file mode 100644 index 0000000..2058832 --- /dev/null +++ b/lib/providers/contact_invitation_list_manager.dart @@ -0,0 +1,583 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:mutex/mutex.dart'; + +import '../entities/entities.dart'; +import '../proto/proto.dart' as proto; +import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; +import 'account.dart'; + +part 'contact_invitation_list_manager.g.dart'; + +////////////////////////////////////////////////// + +class ContactInviteInvalidKeyException implements Exception { + const ContactInviteInvalidKeyException(this.type) : super(); + final EncryptionKeyType type; +} + +typedef GetEncryptionKeyCallback = Future Function( + VeilidCryptoSystem cs, + EncryptionKeyType encryptionKeyType, + Uint8List encryptedSecret); + +////////////////////////////////////////////////// +@immutable +class AcceptedContact { + const AcceptedContact({ + required this.profile, + required this.remoteIdentity, + required this.remoteConversationRecordKey, + required this.localConversationRecordKey, + }); + + final proto.Profile profile; + final IdentityMaster remoteIdentity; + final TypedKey remoteConversationRecordKey; + final TypedKey localConversationRecordKey; +} + +@immutable +class InvitationStatus { + const InvitationStatus({required this.acceptedContact}); + final AcceptedContact? acceptedContact; +} + +////////////////////////////////////////////////// + +////////////////////////////////////////////////// +// Mutable state for per-account contact invitations +@riverpod +class ContactInvitationListManager extends _$ContactInvitationListManager { + ContactInvitationListManager._({ + required ActiveAccountInfo activeAccountInfo, + required DHTShortArray dhtRecord, + }) : _activeAccountInfo = activeAccountInfo, + _dhtRecord = dhtRecord, + _records = IList(); + + @override + FutureOr> build( + ActiveAccountInfo activeAccountInfo) async { + // Load initial todo list from the remote repository + ref.onDispose xxxx call close and pass dhtrecord through... could use a context object + and a DHTValueChangeProvider that we watch in build that updates when dht records change + return _open(activeAccountInfo); + } + + static Future _open( + ActiveAccountInfo activeAccountInfo) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final dhtRecord = await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.contactInvitationRecords), + parent: accountRecordKey); + + return ContactInvitationListManager._( + activeAccountInfo: activeAccountInfo, dhtRecord: dhtRecord); + } + + Future close() async { + state = ""; + await _dhtRecord.close(); + } + + Future refresh() async { + for (var i = 0; i < _dhtRecord.length; i++) { + final cir = await _dhtRecord.getItem(i); + if (cir == null) { + throw Exception('Failed to get contact invitation record'); + } + _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir)); + } + } + + Future createInvitation( + {required EncryptionKeyType encryptionKeyType, + required String encryptionKey, + required String message, + required Timestamp? expiration}) async { + final pool = await DHTRecordPool.instance(); + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final identityKey = + _activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final identitySecret = _activeAccountInfo.userLogin.identitySecret.value; + + // Generate writer keypair to share with new contact + final cs = await pool.veilid.bestCryptoSystem(); + final contactRequestWriter = await cs.generateKeyPair(); + final conversationWriter = _activeAccountInfo.getConversationWriter(); + + // Encrypt the writer secret with the encryption key + final encryptedSecret = await encryptSecretToBytes( + secret: contactRequestWriter.secret, + cryptoKind: cs.kind(), + encryptionKey: encryptionKey, + encryptionKeyType: encryptionKeyType); + + // Create local chat DHT record with the account record key as its parent + // Do not set the encryption of this key yet as it will not yet be written + // to and it will be eventually encrypted with the DH of the contact's + // identity key + late final Uint8List signedContactInvitationBytes; + await (await pool.create( + parent: accountRecordKey, + schema: DHTSchema.smpl(oCnt: 0, members: [ + DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) + ]))) + .deleteScope((localConversation) async { + // dont bother reopening localConversation with writer + // Make ContactRequestPrivate and encrypt with the writer secret + final crpriv = proto.ContactRequestPrivate() + ..writerKey = contactRequestWriter.key.toProto() + ..profile = _activeAccountInfo.account.profile + ..identityMasterRecordKey = + _activeAccountInfo.userLogin.accountMasterRecordKey.toProto() + ..chatRecordKey = localConversation.key.toProto() + ..expiration = expiration?.toInt64() ?? Int64.ZERO; + final crprivbytes = crpriv.writeToBuffer(); + final encryptedContactRequestPrivate = await cs.encryptAeadWithNonce( + crprivbytes, contactRequestWriter.secret); + + // Create ContactRequest and embed contactrequestprivate + final creq = proto.ContactRequest() + ..encryptionKeyType = encryptionKeyType.toProto() + ..private = encryptedContactRequestPrivate; + + // Create DHT unicast inbox for ContactRequest + await (await pool.create( + parent: accountRecordKey, + schema: DHTSchema.smpl(oCnt: 1, members: [ + DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) + ]), + crypto: const DHTRecordCryptoPublic())) + .deleteScope((contactRequestInbox) async { + // Store ContactRequest in owner subkey + await contactRequestInbox.eventualWriteProtobuf(creq); + + // Create ContactInvitation and SignedContactInvitation + final cinv = proto.ContactInvitation() + ..contactRequestInboxKey = contactRequestInbox.key.toProto() + ..writerSecret = encryptedSecret; + final cinvbytes = cinv.writeToBuffer(); + final scinv = proto.SignedContactInvitation() + ..contactInvitation = cinvbytes + ..identitySignature = + (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); + signedContactInvitationBytes = scinv.writeToBuffer(); + + // Create ContactInvitationRecord + final cinvrec = proto.ContactInvitationRecord() + ..contactRequestInbox = + contactRequestInbox.ownedDHTRecordPointer.toProto() + ..writerKey = contactRequestWriter.key.toProto() + ..writerSecret = contactRequestWriter.secret.toProto() + ..localConversationRecordKey = localConversation.key.toProto() + ..expiration = expiration?.toInt64() ?? Int64.ZERO + ..invitation = signedContactInvitationBytes + ..message = message; + + // Add ContactInvitationRecord to account's list + // if this fails, don't keep retrying, user can try again later + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + _activeAccountInfo.account.contactInvitationRecords), + parent: accountRecordKey)) + .scope((cirList) async { + if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) { + throw Exception('Failed to add contact invitation record'); + } + }); + }); + }); + + return signedContactInvitationBytes; + } + + Future deleteInvitation( + {required bool accepted, + required proto.ContactInvitationRecord contactInvitationRecord}) async { + final pool = await DHTRecordPool.instance(); + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + // Remove ContactInvitationRecord from account's list + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + _activeAccountInfo.account.contactInvitationRecords), + parent: accountRecordKey)) + .scope((cirList) async { + for (var i = 0; i < cirList.length; i++) { + final item = await cirList.getItemProtobuf( + proto.ContactInvitationRecord.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact invitation record'); + } + if (item.contactRequestInbox.recordKey == + contactInvitationRecord.contactRequestInbox.recordKey) { + await cirList.tryRemoveItem(i); + break; + } + } + await (await pool.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + contactInvitationRecord.contactRequestInbox), + parent: accountRecordKey)) + .scope((contactRequestInbox) async { + // Wipe out old invitation so it shows up as invalid + await contactRequestInbox.tryWriteBytes(Uint8List(0)); + await contactRequestInbox.delete(); + }); + if (!accepted) { + await (await pool.openRead( + proto.TypedKeyProto.fromProto( + contactInvitationRecord.localConversationRecordKey), + parent: accountRecordKey)) + .delete(); + } + }); + } + + Future validateInvitation( + {required Uint8List inviteData, + required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final signedContactInvitation = + proto.SignedContactInvitation.fromBuffer(inviteData); + + final contactInvitationBytes = + Uint8List.fromList(signedContactInvitation.contactInvitation); + final contactInvitation = + proto.ContactInvitation.fromBuffer(contactInvitationBytes); + + final contactRequestInboxKey = + proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey); + + ValidContactInvitation? out; + + final pool = await DHTRecordPool.instance(); + final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); + + // See if we're chatting to ourselves, if so, don't delete it here + final isSelf = _records.indexWhere((cir) => + proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == + contactRequestInboxKey) != + -1; + + await (await pool.openRead(contactRequestInboxKey, + parent: accountRecordKey)) + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + // + final contactRequest = await contactRequestInbox + .getProtobuf(proto.ContactRequest.fromBuffer); + + // Decrypt contact request private + final encryptionKeyType = + EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType); + late final SharedSecret? writerSecret; + try { + writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType, + Uint8List.fromList(contactInvitation.writerSecret)); + } on Exception catch (_) { + throw ContactInviteInvalidKeyException(encryptionKeyType); + } + if (writerSecret == null) { + return null; + } + + final contactRequestPrivateBytes = await cs.decryptAeadWithNonce( + Uint8List.fromList(contactRequest.private), writerSecret); + + final contactRequestPrivate = + proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes); + final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( + contactRequestPrivate.identityMasterRecordKey); + + // Fetch the account master + final contactIdentityMaster = await openIdentityMaster( + identityMasterRecordKey: contactIdentityMasterRecordKey); + + // Verify + final signature = proto.SignatureProto.fromProto( + signedContactInvitation.identitySignature); + await cs.verify(contactIdentityMaster.identityPublicKey, + contactInvitationBytes, signature); + + final writer = KeyPair( + key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey), + secret: writerSecret); + + out = ValidContactInvitation._( + contactInvitationManager: this, + signedContactInvitation: signedContactInvitation, + contactInvitation: contactInvitation, + contactRequestInboxKey: contactRequestInboxKey, + contactRequest: contactRequest, + contactRequestPrivate: contactRequestPrivate, + contactIdentityMaster: contactIdentityMaster, + writer: writer); + }); + + return out; + } + + Future checkInvitationStatus( + {required ActiveAccountInfo activeAccountInfo, + required proto.ContactInvitationRecord contactInvitationRecord}) async { + // Open the contact request inbox + try { + final pool = await DHTRecordPool.instance(); + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final writerKey = + proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); + final writerSecret = + proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret); + final recordKey = proto.TypedKeyProto.fromProto( + contactInvitationRecord.contactRequestInbox.recordKey); + final writer = TypedKeyPair( + kind: recordKey.kind, key: writerKey, secret: writerSecret); + final acceptReject = await (await pool.openRead(recordKey, + crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + parent: accountRecordKey, + defaultSubkey: 1)) + .scope((contactRequestInbox) async { + // + final signedContactResponse = await contactRequestInbox.getProtobuf( + proto.SignedContactResponse.fromBuffer, + forceRefresh: true); + if (signedContactResponse == null) { + return null; + } + + final contactResponseBytes = + Uint8List.fromList(signedContactResponse.contactResponse); + final contactResponse = + proto.ContactResponse.fromBuffer(contactResponseBytes); + final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( + contactResponse.identityMasterRecordKey); + final cs = await pool.veilid.getCryptoSystem(recordKey.kind); + + // Fetch the remote contact's account master + final contactIdentityMaster = await openIdentityMaster( + identityMasterRecordKey: contactIdentityMasterRecordKey); + + // Verify + final signature = proto.SignatureProto.fromProto( + signedContactResponse.identitySignature); + await cs.verify(contactIdentityMaster.identityPublicKey, + contactResponseBytes, signature); + + // Check for rejection + if (!contactResponse.accept) { + return const InvitationStatus(acceptedContact: null); + } + + // Pull profile from remote conversation key + final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( + contactResponse.remoteConversationRecordKey); + final remoteConversation = await readRemoteConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), + remoteConversationRecordKey: remoteConversationRecordKey); + if (remoteConversation == null) { + log.info('Remote conversation could not be read. Waiting...'); + return null; + } + // Complete the local conversation now that we have the remote profile + final localConversationRecordKey = proto.TypedKeyProto.fromProto( + contactInvitationRecord.localConversationRecordKey); + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), + existingConversationRecordKey: localConversationRecordKey, + // ignore: prefer_expression_function_bodies + callback: (localConversation) async { + return InvitationStatus( + acceptedContact: AcceptedContact( + profile: remoteConversation.profile, + remoteIdentity: contactIdentityMaster, + remoteConversationRecordKey: remoteConversationRecordKey, + localConversationRecordKey: localConversationRecordKey)); + }); + }); + + if (acceptReject == null) { + return null; + } + + // Delete invitation and return the accepted or rejected contact + await deleteInvitation( + accepted: acceptReject.acceptedContact != null, + contactInvitationRecord: contactInvitationRecord); + + return acceptReject; + } on Exception catch (e) { + log.error('Exception in checkAcceptRejectContact: $e', e); + + // Attempt to clean up. All this needs better lifetime management + await deleteInvitation( + accepted: false, contactInvitationRecord: contactInvitationRecord); + + rethrow; + } + } + + // + + final ActiveAccountInfo _activeAccountInfo; + 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/lib/providers/contact_invitation_list_manager.g.dart b/lib/providers/contact_invitation_list_manager.g.dart new file mode 100644 index 0000000..ce2f160 --- /dev/null +++ b/lib/providers/contact_invitation_list_manager.g.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contact_invitation_list_manager.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$contactInvitationListManagerHash() => + r'8dda8e5005f0c0c921e3e8b7ce06e54bb5682085'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ContactInvitationListManager + extends BuildlessAutoDisposeAsyncNotifier< + IList> { + late final ActiveAccountInfo activeAccountInfo; + + FutureOr> build( + ActiveAccountInfo activeAccountInfo, + ); +} + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +@ProviderFor(ContactInvitationListManager) +const contactInvitationListManagerProvider = + ContactInvitationListManagerFamily(); + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +class ContactInvitationListManagerFamily + extends Family>> { + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + const ContactInvitationListManagerFamily(); + + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + ContactInvitationListManagerProvider call( + ActiveAccountInfo activeAccountInfo, + ) { + return ContactInvitationListManagerProvider( + activeAccountInfo, + ); + } + + @override + ContactInvitationListManagerProvider getProviderOverride( + covariant ContactInvitationListManagerProvider provider, + ) { + return call( + provider.activeAccountInfo, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'contactInvitationListManagerProvider'; +} + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +class ContactInvitationListManagerProvider + extends AutoDisposeAsyncNotifierProviderImpl> { + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + ContactInvitationListManagerProvider( + ActiveAccountInfo activeAccountInfo, + ) : this._internal( + () => ContactInvitationListManager() + ..activeAccountInfo = activeAccountInfo, + from: contactInvitationListManagerProvider, + name: r'contactInvitationListManagerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$contactInvitationListManagerHash, + dependencies: ContactInvitationListManagerFamily._dependencies, + allTransitiveDependencies: + ContactInvitationListManagerFamily._allTransitiveDependencies, + activeAccountInfo: activeAccountInfo, + ); + + ContactInvitationListManagerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.activeAccountInfo, + }) : super.internal(); + + final ActiveAccountInfo activeAccountInfo; + + @override + FutureOr> runNotifierBuild( + covariant ContactInvitationListManager notifier, + ) { + return notifier.build( + activeAccountInfo, + ); + } + + @override + Override overrideWith(ContactInvitationListManager Function() create) { + return ProviderOverride( + origin: this, + override: ContactInvitationListManagerProvider._internal( + () => create()..activeAccountInfo = activeAccountInfo, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + activeAccountInfo: activeAccountInfo, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement> createElement() { + return _ContactInvitationListManagerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ContactInvitationListManagerProvider && + other.activeAccountInfo == activeAccountInfo; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, activeAccountInfo.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ContactInvitationListManagerRef on AutoDisposeAsyncNotifierProviderRef< + IList> { + /// The parameter `activeAccountInfo` of this provider. + ActiveAccountInfo get activeAccountInfo; +} + +class _ContactInvitationListManagerProviderElement + extends AutoDisposeAsyncNotifierProviderElement< + ContactInvitationListManager, IList> + with ContactInvitationListManagerRef { + _ContactInvitationListManagerProviderElement(super.provider); + + @override + ActiveAccountInfo get activeAccountInfo => + (origin as ContactInvitationListManagerProvider).activeAccountInfo; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invite.dart b/lib/providers/contact_invite.dart index e776409..bdbb8f7 100644 --- a/lib/providers/contact_invite.dart +++ b/lib/providers/contact_invite.dart @@ -13,509 +13,6 @@ import 'conversation.dart'; part 'contact_invite.g.dart'; -class ContactInviteInvalidKeyException implements Exception { - const ContactInviteInvalidKeyException(this.type) : super(); - final EncryptionKeyType type; -} - -class AcceptedContact { - AcceptedContact({ - required this.profile, - required this.remoteIdentity, - required this.remoteConversationRecordKey, - required this.localConversationRecordKey, - }); - - proto.Profile profile; - IdentityMaster remoteIdentity; - TypedKey remoteConversationRecordKey; - TypedKey localConversationRecordKey; -} - -class AcceptedOrRejectedContact { - AcceptedOrRejectedContact({required this.acceptedContact}); - AcceptedContact? acceptedContact; -} - -Future checkAcceptRejectContact( - {required ActiveAccountInfo activeAccountInfo, - required proto.ContactInvitationRecord contactInvitationRecord}) async { - // Open the contact request inbox - try { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final writerKey = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); - final writerSecret = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret); - final recordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.contactRequestInbox.recordKey); - final writer = TypedKeyPair( - kind: recordKey.kind, key: writerKey, secret: writerSecret); - final acceptReject = await (await pool.openRead(recordKey, - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - parent: accountRecordKey, - defaultSubkey: 1)) - .scope((contactRequestInbox) async { - // - final signedContactResponse = await contactRequestInbox.getProtobuf( - proto.SignedContactResponse.fromBuffer, - forceRefresh: true); - if (signedContactResponse == null) { - return null; - } - - final contactResponseBytes = - Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = - proto.ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.identityMasterRecordKey); - final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - - // Fetch the remote contact's account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); - - // Verify - final signature = proto.SignatureProto.fromProto( - signedContactResponse.identitySignature); - await cs.verify(contactIdentityMaster.identityPublicKey, - contactResponseBytes, signature); - - // Check for rejection - if (!contactResponse.accept) { - return AcceptedOrRejectedContact(acceptedContact: null); - } - - // Pull profile from remote conversation key - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.remoteConversationRecordKey); - final remoteConversation = await readRemoteConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), - remoteConversationRecordKey: remoteConversationRecordKey); - if (remoteConversation == null) { - log.info('Remote conversation could not be read. Waiting...'); - return null; - } - // Complete the local conversation now that we have the remote profile - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey); - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), - existingConversationRecordKey: localConversationRecordKey, - // ignore: prefer_expression_function_bodies - callback: (localConversation) async { - return AcceptedOrRejectedContact( - acceptedContact: AcceptedContact( - profile: remoteConversation.profile, - remoteIdentity: contactIdentityMaster, - remoteConversationRecordKey: remoteConversationRecordKey, - localConversationRecordKey: localConversationRecordKey)); - }); - }); - - if (acceptReject == null) { - return null; - } - - // Delete invitation and return the accepted or rejected contact - await deleteContactInvitation( - accepted: acceptReject.acceptedContact != null, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - - return acceptReject; - } on Exception catch (e) { - log.error('Exception in checkAcceptRejectContact: $e', e); - - // Attempt to clean up. All this needs better lifetime management - await deleteContactInvitation( - accepted: false, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - - rethrow; - } -} - -Future deleteContactInvitation( - {required bool accepted, - required ActiveAccountInfo activeAccountInfo, - required proto.ContactInvitationRecord contactInvitationRecord}) async { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Remove ContactInvitationRecord from account's list - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactInvitationRecords), - parent: accountRecordKey)) - .scope((cirList) async { - for (var i = 0; i < cirList.length; i++) { - final item = await cirList.getItemProtobuf( - proto.ContactInvitationRecord.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact invitation record'); - } - if (item.contactRequestInbox.recordKey == - contactInvitationRecord.contactRequestInbox.recordKey) { - await cirList.tryRemoveItem(i); - break; - } - } - await (await pool.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - contactInvitationRecord.contactRequestInbox), - parent: accountRecordKey)) - .scope((contactRequestInbox) async { - // Wipe out old invitation so it shows up as invalid - await contactRequestInbox.tryWriteBytes(Uint8List(0)); - await contactRequestInbox.delete(); - }); - if (!accepted) { - await (await pool.openRead( - proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey), - parent: accountRecordKey)) - .delete(); - } - }); -} - -Future createContactInvitation( - {required ActiveAccountInfo activeAccountInfo, - required EncryptionKeyType encryptionKeyType, - required String encryptionKey, - required String message, - required Timestamp? expiration}) async { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final identityKey = - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final identitySecret = activeAccountInfo.userLogin.identitySecret.value; - - // Generate writer keypair to share with new contact - final cs = await pool.veilid.bestCryptoSystem(); - final contactRequestWriter = await cs.generateKeyPair(); - final conversationWriter = - getConversationWriter(activeAccountInfo: activeAccountInfo); - - // Encrypt the writer secret with the encryption key - final encryptedSecret = await encryptSecretToBytes( - secret: contactRequestWriter.secret, - cryptoKind: cs.kind(), - encryptionKey: encryptionKey, - encryptionKeyType: encryptionKeyType); - - // Create local chat DHT record with the account record key as its parent - // Do not set the encryption of this key yet as it will not yet be written - // to and it will be eventually encrypted with the DH of the contact's - // identity key - late final Uint8List signedContactInvitationBytes; - await (await pool.create( - parent: accountRecordKey, - schema: DHTSchema.smpl(oCnt: 0, members: [ - DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) - ]))) - .deleteScope((localConversation) async { - // dont bother reopening localConversation with writer - // Make ContactRequestPrivate and encrypt with the writer secret - final crpriv = proto.ContactRequestPrivate() - ..writerKey = contactRequestWriter.key.toProto() - ..profile = activeAccountInfo.account.profile - ..identityMasterRecordKey = - activeAccountInfo.userLogin.accountMasterRecordKey.toProto() - ..chatRecordKey = localConversation.key.toProto() - ..expiration = expiration?.toInt64() ?? Int64.ZERO; - final crprivbytes = crpriv.writeToBuffer(); - final encryptedContactRequestPrivate = - await cs.encryptAeadWithNonce(crprivbytes, contactRequestWriter.secret); - - // Create ContactRequest and embed contactrequestprivate - final creq = proto.ContactRequest() - ..encryptionKeyType = encryptionKeyType.toProto() - ..private = encryptedContactRequestPrivate; - - // Create DHT unicast inbox for ContactRequest - await (await pool.create( - parent: accountRecordKey, - schema: DHTSchema.smpl(oCnt: 1, members: [ - DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) - ]), - crypto: const DHTRecordCryptoPublic())) - .deleteScope((contactRequestInbox) async { - // Store ContactRequest in owner subkey - await contactRequestInbox.eventualWriteProtobuf(creq); - - // Create ContactInvitation and SignedContactInvitation - final cinv = proto.ContactInvitation() - ..contactRequestInboxKey = contactRequestInbox.key.toProto() - ..writerSecret = encryptedSecret; - final cinvbytes = cinv.writeToBuffer(); - final scinv = proto.SignedContactInvitation() - ..contactInvitation = cinvbytes - ..identitySignature = - (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); - signedContactInvitationBytes = scinv.writeToBuffer(); - - // Create ContactInvitationRecord - final cinvrec = proto.ContactInvitationRecord() - ..contactRequestInbox = - contactRequestInbox.ownedDHTRecordPointer.toProto() - ..writerKey = contactRequestWriter.key.toProto() - ..writerSecret = contactRequestWriter.secret.toProto() - ..localConversationRecordKey = localConversation.key.toProto() - ..expiration = expiration?.toInt64() ?? Int64.ZERO - ..invitation = signedContactInvitationBytes - ..message = message; - - // Add ContactInvitationRecord to account's list - // if this fails, don't keep retrying, user can try again later - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactInvitationRecords), - parent: accountRecordKey)) - .scope((cirList) async { - if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) { - throw Exception('Failed to add contact invitation record'); - } - }); - }); - }); - - return signedContactInvitationBytes; -} - -class ValidContactInvitation { - ValidContactInvitation( - {required this.signedContactInvitation, - required this.contactInvitation, - required this.contactRequestInboxKey, - required this.contactRequest, - required this.contactRequestPrivate, - required this.contactIdentityMaster, - required this.writer}); - - proto.SignedContactInvitation signedContactInvitation; - proto.ContactInvitation contactInvitation; - TypedKey contactRequestInboxKey; - proto.ContactRequest contactRequest; - proto.ContactRequestPrivate contactRequestPrivate; - IdentityMaster contactIdentityMaster; - KeyPair writer; -} - -typedef GetEncryptionKeyCallback = Future Function( - VeilidCryptoSystem cs, - EncryptionKeyType encryptionKeyType, - Uint8List encryptedSecret); - -Future validateContactInvitation( - {required ActiveAccountInfo activeAccountInfo, - required IList? contactInvitationRecords, - required Uint8List inviteData, - required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - final signedContactInvitation = - proto.SignedContactInvitation.fromBuffer(inviteData); - - final contactInvitationBytes = - Uint8List.fromList(signedContactInvitation.contactInvitation); - final contactInvitation = - proto.ContactInvitation.fromBuffer(contactInvitationBytes); - - final contactRequestInboxKey = - proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey); - - ValidContactInvitation? out; - - final pool = await DHTRecordPool.instance(); - final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); - - // See if we're chatting to ourselves, if so, don't delete it here - final isSelf = contactInvitationRecords?.indexWhere((cir) => - proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == - contactRequestInboxKey) != - -1; - - await (await pool.openRead(contactRequestInboxKey, parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - // - final contactRequest = - await contactRequestInbox.getProtobuf(proto.ContactRequest.fromBuffer); - - // Decrypt contact request private - final encryptionKeyType = - EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType); - late final SharedSecret? writerSecret; - try { - writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType, - Uint8List.fromList(contactInvitation.writerSecret)); - } on Exception catch (_) { - throw ContactInviteInvalidKeyException(encryptionKeyType); - } - if (writerSecret == null) { - return null; - } - - final contactRequestPrivateBytes = await cs.decryptAeadWithNonce( - Uint8List.fromList(contactRequest.private), writerSecret); - - final contactRequestPrivate = - proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactRequestPrivate.identityMasterRecordKey); - - // Fetch the account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); - - // Verify - final signature = proto.SignatureProto.fromProto( - signedContactInvitation.identitySignature); - await cs.verify(contactIdentityMaster.identityPublicKey, - contactInvitationBytes, signature); - - final writer = KeyPair( - key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey), - secret: writerSecret); - - out = ValidContactInvitation( - signedContactInvitation: signedContactInvitation, - contactInvitation: contactInvitation, - contactRequestInboxKey: contactRequestInboxKey, - contactRequest: contactRequest, - contactRequestPrivate: contactRequestPrivate, - contactIdentityMaster: contactIdentityMaster, - writer: writer); - }); - - return out; -} - -Future acceptContactInvitation( - ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - try { - // Ensure we don't delete this if we're trying to chat to self - final isSelf = - validContactInvitation.contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.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: validContactInvitation.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( - validContactInvitation.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: validContactInvitation.contactRequestPrivate.profile, - remoteIdentity: validContactInvitation.contactIdentityMaster, - remoteConversationRecordKey: proto.TypedKeyProto.fromProto( - validContactInvitation.contactRequestPrivate.chatRecordKey), - localConversationRecordKey: localConversation.key, - ); - }); - }); - } on Exception catch (e) { - log.debug('exception: $e', e); - return null; - } -} - -Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - - // Ensure we don't delete this if we're trying to chat to self - final isSelf = - validContactInvitation.contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer, - parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - final cs = await pool.veilid - .getCryptoSystem(validContactInvitation.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; - }); -} - /// Get the active account contact invitation list @riverpod Future?> fetchContactInvitationRecords( diff --git a/lib/providers/contact_invite.g.dart b/lib/providers/contact_invite.g.dart index b6cf257..758a54a 100644 --- a/lib/providers/contact_invite.g.dart +++ b/lib/providers/contact_invite.g.dart @@ -7,14 +7,14 @@ part of 'contact_invite.dart'; // ************************************************************************** String _$fetchContactInvitationRecordsHash() => - r'365d563c5e66f45679f597502ea9e4b8296ff8af'; + r'ff0b2c68d42cb106602982b1fb56a7bd8183c04a'; /// Get the active account contact invitation list /// /// Copied from [fetchContactInvitationRecords]. @ProviderFor(fetchContactInvitationRecords) final fetchContactInvitationRecordsProvider = - AutoDisposeFutureProvider?>.internal( + AutoDisposeFutureProvider?>.internal( fetchContactInvitationRecords, name: r'fetchContactInvitationRecordsProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -25,6 +25,6 @@ final fetchContactInvitationRecordsProvider = ); typedef FetchContactInvitationRecordsRef - = AutoDisposeFutureProviderRef?>; + = AutoDisposeFutureProviderRef?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index 617ead3..bac8cda 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -1,3 +1,7 @@ +// A Conversation is a type of Chat that is 1:1 between two Contacts only +// Each Contact in the ContactList has at most one Conversation between the +// remote contact and the local account + import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -14,25 +18,230 @@ import 'contact.dart'; part 'conversation.g.dart'; -Future getConversationCrypto({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, -}) async { - final veilid = await eventualVeilid.future; - final identitySecret = activeAccountInfo.userLogin.identitySecret; - final cs = await veilid.getCryptoSystem(identitySecret.kind); - final sharedSecret = - await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value); - return DHTRecordCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); -} +class Conversation { + Conversation._( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey localConversationRecordKey, + required TypedKey remoteIdentityPublicKey, + required TypedKey remoteConversationRecordKey}) + : _activeAccountInfo = activeAccountInfo, + _localConversationRecordKey = localConversationRecordKey, + _remoteIdentityPublicKey = remoteIdentityPublicKey, + _remoteConversationRecordKey = remoteConversationRecordKey; -KeyPair getConversationWriter({ - required ActiveAccountInfo activeAccountInfo, -}) { - final identityKey = - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final identitySecret = activeAccountInfo.userLogin.identitySecret; - return KeyPair(key: identityKey, secret: identitySecret.value); + Future open() async {} + + Future close() async { + // + } + + Future readRemoteConversation() async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + return (await pool.openRead(_remoteConversationRecordKey, + parent: accountRecordKey, crypto: crypto)) + .scope((remoteConversation) async { + // + final conversation = + await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); + return conversation; + }); + } + + Future readLocalConversation() async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + return (await pool.openRead(_localConversationRecordKey, + parent: accountRecordKey, crypto: crypto)) + .scope((localConversation) async { + // + final update = + await localConversation.getProtobuf(proto.Conversation.fromBuffer); + if (update != null) { + return update; + } + return null; + }); + } + + Future writeLocalConversation({ + required proto.Conversation conversation, + }) async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + return (await pool.openWrite(_localConversationRecordKey, writer, + parent: accountRecordKey, crypto: crypto)) + .scope((localConversation) async { + // + final update = await localConversation.tryWriteProtobuf( + proto.Conversation.fromBuffer, conversation); + if (update != null) { + return update; + } + return null; + }); + } + + Future addLocalConversationMessage( + {required proto.Message message}) async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + await (await DHTShortArray.openWrite(messagesRecordKey, writer, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + await messages.tryAddItem(message.writeToBuffer()); + }); + } + + Future mergeLocalConversationMessages( + {required IList newMessages}) async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return false; + } + var changed = false; + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) + .compareTo(Timestamp.fromInt64(b.timestamp))); + + await (await DHTShortArray.openWrite(messagesRecordKey, writer, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + // Ensure newMessages is sorted by timestamp + newMessages = + newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // Existing messages will always be sorted by timestamp so merging is easy + var pos = 0; + outer: + for (final newMessage in newMessages) { + var skip = false; + while (pos < messages.length) { + final m = + await messages.getItemProtobuf(proto.Message.fromBuffer, pos); + if (m == null) { + log.error('unable to get message #$pos'); + break outer; + } + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final curTs = Timestamp.fromInt64(m.timestamp); + final cmp = newTs.compareTo(curTs); + if (cmp < 0) { + break; + } else if (cmp == 0) { + skip = true; + break; + } + pos++; + } + // Insert at this position + if (!skip) { + await messages.tryInsertItem(pos, newMessage.writeToBuffer()); + changed = true; + } + } + }); + return changed; + } + + Future?> getRemoteConversationMessages() async { + final conversation = await readRemoteConversation(); + if (conversation == null) { + return null; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + + return (await DHTShortArray.openRead(messagesRecordKey, + parent: _remoteConversationRecordKey, crypto: crypto)) + .scope((messages) async { + var out = IList(); + for (var i = 0; i < messages.length; i++) { + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); + if (msg == null) { + throw Exception('Failed to get message'); + } + out = out.add(msg); + } + return out; + }); + } + + // + + Future getConversationCrypto() async { + var conversationCrypto = _conversationCrypto; + if (conversationCrypto != null) { + return conversationCrypto; + } + final veilid = await eventualVeilid.future; + final identitySecret = _activeAccountInfo.userLogin.identitySecret; + final cs = await veilid.getCryptoSystem(identitySecret.kind); + final sharedSecret = + await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); + + conversationCrypto = await DHTRecordCryptoPrivate.fromSecret( + identitySecret.kind, sharedSecret); + _conversationCrypto = conversationCrypto; + return conversationCrypto; + } + + Future?> getLocalConversationMessages() async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return null; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + + return (await DHTShortArray.openRead(messagesRecordKey, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + var out = IList(); + for (var i = 0; i < messages.length; i++) { + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); + if (msg == null) { + throw Exception('Failed to get message'); + } + out = out.add(msg); + } + return out; + }); + } + + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _localConversationRecordKey; + final TypedKey _remoteIdentityPublicKey; + final TypedKey _remoteConversationRecordKey; + // + DHTRecordCrypto? _conversationCrypto; } // Create a conversation @@ -51,7 +260,7 @@ Future createConversation( final crypto = await getConversationCrypto( activeAccountInfo: activeAccountInfo, remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); + final writer = activeAccountInfo.getConversationWriter(); // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -95,238 +304,10 @@ Future createConversation( }); } -Future readRemoteConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - return (await pool.openRead(remoteConversationRecordKey, - parent: accountRecordKey, crypto: crypto)) - .scope((remoteConversation) async { - // - final conversation = - await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); - return conversation; - }); -} - -Future writeLocalConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required proto.Conversation conversation, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - return (await pool.openWrite(localConversationRecordKey, writer, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - return update; - } - return null; - }); -} - -Future readLocalConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await pool.openRead(localConversationRecordKey, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = - await localConversation.getProtobuf(proto.Conversation.fromBuffer); - if (update != null) { - return update; - } - return null; - }); -} - -Future addLocalConversationMessage( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required proto.Message message}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - await messages.tryAddItem(message.writeToBuffer()); - }); -} - -Future mergeLocalConversationMessages( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required IList newMessages}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return false; - } - var changed = false; - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) - .compareTo(Timestamp.fromInt64(b.timestamp))); - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - // Ensure newMessages is sorted by timestamp - newMessages = - newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // Existing messages will always be sorted by timestamp so merging is easy - var pos = 0; - outer: - for (final newMessage in newMessages) { - var skip = false; - while (pos < messages.length) { - final m = await messages.getItemProtobuf(proto.Message.fromBuffer, pos); - if (m == null) { - log.error('unable to get message #$pos'); - break outer; - } - - // If timestamp to insert is less than - // the current position, insert it here - final newTs = Timestamp.fromInt64(newMessage.timestamp); - final curTs = Timestamp.fromInt64(m.timestamp); - final cmp = newTs.compareTo(curTs); - if (cmp < 0) { - break; - } else if (cmp == 0) { - skip = true; - break; - } - pos++; - } - // Insert at this position - if (!skip) { - await messages.tryInsertItem(pos, newMessage.writeToBuffer()); - changed = true; - } - } - }); - return changed; -} - -Future?> getLocalConversationMessages({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); -} - -Future?> getRemoteConversationMessages({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final conversation = await readRemoteConversation( - activeAccountInfo: activeAccountInfo, - remoteConversationRecordKey: remoteConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: remoteConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); -} +// +// +// +// @riverpod class ActiveConversationMessages extends _$ActiveConversationMessages { diff --git a/lib/providers/conversation.g.dart b/lib/providers/conversation.g.dart index fcf007c..a4875dd 100644 --- a/lib/providers/conversation.g.dart +++ b/lib/providers/conversation.g.dart @@ -7,12 +7,12 @@ part of 'conversation.dart'; // ************************************************************************** String _$activeConversationMessagesHash() => - r'61c9e16f1304c7929a971ec7711d2b6c7cadc5ea'; + r'5579a9386f2046b156720ae799a0e77aca119b09'; /// See also [ActiveConversationMessages]. @ProviderFor(ActiveConversationMessages) final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider< - ActiveConversationMessages, IList?>.internal( + ActiveConversationMessages, IList?>.internal( ActiveConversationMessages.new, name: r'activeConversationMessagesProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -23,6 +23,6 @@ final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider< ); typedef _$ActiveConversationMessages - = AutoDisposeAsyncNotifier?>; + = AutoDisposeAsyncNotifier?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/local_accounts.dart b/lib/providers/local_accounts.dart index c7c793d..4294adf 100644 --- a/lib/providers/local_accounts.dart +++ b/lib/providers/local_accounts.dart @@ -14,7 +14,7 @@ part 'local_accounts.g.dart'; const String veilidChatAccountKey = 'com.veilid.veilidchat'; -// Local account manager +// Local accounts table @riverpod class LocalAccounts extends _$LocalAccounts with AsyncTableDBBacked> {