refactoring

This commit is contained in:
Christien Rioux 2023-12-21 12:10:54 -05:00
parent d3ecae0113
commit e898074387
12 changed files with 1100 additions and 822 deletions

View File

View File

@ -0,0 +1 @@

View File

@ -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<AccountInfo> fetchAccount(FetchAccountRef ref,
Future<AccountInfo> 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<AccountInfo> 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<AccountInfo> 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<ActiveAccountInfo?> fetchActiveAccount(FetchActiveAccountRef ref) async {
Future<ActiveAccountInfo?> 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));

View File

@ -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<AsyncValue<AccountInfo>> {
/// Copied from [fetchAccountInfo].
class FetchAccountInfoFamily extends Family<AsyncValue<AccountInfo>> {
/// 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<FixedEncodedString43> 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<AsyncValue<AccountInfo>> {
_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<AccountInfo> {
/// Copied from [fetchAccountInfo].
class FetchAccountInfoProvider extends AutoDisposeFutureProvider<AccountInfo> {
/// 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<FixedEncodedString43> 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<AccountInfo> {
@override
Override overrideWith(
FutureOr<AccountInfo> Function(FetchAccountRef provider) create,
FutureOr<AccountInfo> 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<AccountInfo> {
@override
AutoDisposeFutureProviderElement<AccountInfo> 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<AccountInfo> {
}
}
mixin FetchAccountRef on AutoDisposeFutureProviderRef<AccountInfo> {
mixin FetchAccountInfoRef on AutoDisposeFutureProviderRef<AccountInfo> {
/// The parameter `accountMasterRecordKey` of this provider.
Typed<FixedEncodedString43> get accountMasterRecordKey;
}
class _FetchAccountProviderElement
extends AutoDisposeFutureProviderElement<AccountInfo> with FetchAccountRef {
_FetchAccountProviderElement(super.provider);
class _FetchAccountInfoProviderElement
extends AutoDisposeFutureProviderElement<AccountInfo>
with FetchAccountInfoRef {
_FetchAccountInfoProviderElement(super.provider);
@override
Typed<FixedEncodedString43> 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<ActiveAccountInfo?>.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<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

View File

@ -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<IList<Chat>?>.internal(
final fetchChatListProvider =
AutoDisposeFutureProvider<IList<proto.Chat>?>.internal(
fetchChatList,
name: r'fetchChatListProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -22,6 +23,6 @@ final fetchChatListProvider = AutoDisposeFutureProvider<IList<Chat>?>.internal(
allTransitiveDependencies: null,
);
typedef FetchChatListRef = AutoDisposeFutureProviderRef<IList<Chat>?>;
typedef FetchChatListRef = AutoDisposeFutureProviderRef<IList<proto.Chat>?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -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<SecretKey?> 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<IList<proto.ContactInvitationRecord>> 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<ContactInvitationListManager> _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<void> close() async {
state = "";
await _dhtRecord.close();
}
Future<void> 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<Uint8List> 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<void> 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<ValidContactInvitation?> 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<InvitationStatus?> 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<proto.ContactInvitationRecord> _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<AcceptedContact?> 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<bool> 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;
}

View File

@ -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<proto.ContactInvitationRecord>> {
late final ActiveAccountInfo activeAccountInfo;
FutureOr<IList<proto.ContactInvitationRecord>> build(
ActiveAccountInfo activeAccountInfo,
);
}
//////////////////////////////////////////////////
//////////////////////////////////////////////////
///
/// Copied from [ContactInvitationListManager].
@ProviderFor(ContactInvitationListManager)
const contactInvitationListManagerProvider =
ContactInvitationListManagerFamily();
//////////////////////////////////////////////////
//////////////////////////////////////////////////
///
/// Copied from [ContactInvitationListManager].
class ContactInvitationListManagerFamily
extends Family<AsyncValue<IList<proto.ContactInvitationRecord>>> {
//////////////////////////////////////////////////
//////////////////////////////////////////////////
///
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'contactInvitationListManagerProvider';
}
//////////////////////////////////////////////////
//////////////////////////////////////////////////
///
/// Copied from [ContactInvitationListManager].
class ContactInvitationListManagerProvider
extends AutoDisposeAsyncNotifierProviderImpl<ContactInvitationListManager,
IList<proto.ContactInvitationRecord>> {
//////////////////////////////////////////////////
//////////////////////////////////////////////////
///
/// 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<IList<proto.ContactInvitationRecord>> 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<ContactInvitationListManager,
IList<proto.ContactInvitationRecord>> 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<proto.ContactInvitationRecord>> {
/// The parameter `activeAccountInfo` of this provider.
ActiveAccountInfo get activeAccountInfo;
}
class _ContactInvitationListManagerProviderElement
extends AutoDisposeAsyncNotifierProviderElement<
ContactInvitationListManager, IList<proto.ContactInvitationRecord>>
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

View File

@ -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<AcceptedOrRejectedContact?> 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<void> 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<Uint8List> 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<SecretKey?> Function(
VeilidCryptoSystem cs,
EncryptionKeyType encryptionKeyType,
Uint8List encryptedSecret);
Future<ValidContactInvitation?> validateContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required IList<proto.ContactInvitationRecord>? 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<AcceptedContact?> 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<bool> 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<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(

View File

@ -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<IList<ContactInvitationRecord>?>.internal(
AutoDisposeFutureProvider<IList<proto.ContactInvitationRecord>?>.internal(
fetchContactInvitationRecords,
name: r'fetchContactInvitationRecordsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -25,6 +25,6 @@ final fetchContactInvitationRecordsProvider =
);
typedef FetchContactInvitationRecordsRef
= AutoDisposeFutureProviderRef<IList<ContactInvitationRecord>?>;
= AutoDisposeFutureProviderRef<IList<proto.ContactInvitationRecord>?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -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<DHTRecordCrypto> getConversationCrypto({
required ActiveAccountInfo activeAccountInfo,
class Conversation {
Conversation._(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey,
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);
required TypedKey remoteConversationRecordKey})
: _activeAccountInfo = activeAccountInfo,
_localConversationRecordKey = localConversationRecordKey,
_remoteIdentityPublicKey = remoteIdentityPublicKey,
_remoteConversationRecordKey = remoteConversationRecordKey;
Future<Conversation> open() async {}
Future<void> close() async {
//
}
KeyPair getConversationWriter({
required ActiveAccountInfo activeAccountInfo,
}) {
final identityKey =
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final identitySecret = activeAccountInfo.userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
Future<proto.Conversation?> 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<proto.Conversation?> 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<proto.Conversation?> 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<void> 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<bool> mergeLocalConversationMessages(
{required IList<proto.Message> 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<IList<proto.Message>?> 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<proto.Message>();
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<DHTRecordCrypto> 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<IList<proto.Message>?> 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<proto.Message>();
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<T> createConversation<T>(
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<T> createConversation<T>(
});
}
Future<proto.Conversation?> 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<proto.Conversation?> 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<proto.Conversation?> 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<void> 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<bool> mergeLocalConversationMessages(
{required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey,
required TypedKey remoteIdentityPublicKey,
required IList<proto.Message> 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<IList<proto.Message>?> 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<proto.Message>();
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<IList<proto.Message>?> 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<proto.Message>();
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 {

View File

@ -7,12 +7,12 @@ part of 'conversation.dart';
// **************************************************************************
String _$activeConversationMessagesHash() =>
r'61c9e16f1304c7929a971ec7711d2b6c7cadc5ea';
r'5579a9386f2046b156720ae799a0e77aca119b09';
/// See also [ActiveConversationMessages].
@ProviderFor(ActiveConversationMessages)
final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider<
ActiveConversationMessages, IList<Message>?>.internal(
ActiveConversationMessages, IList<proto.Message>?>.internal(
ActiveConversationMessages.new,
name: r'activeConversationMessagesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -23,6 +23,6 @@ final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider<
);
typedef _$ActiveConversationMessages
= AutoDisposeAsyncNotifier<IList<Message>?>;
= AutoDisposeAsyncNotifier<IList<proto.Message>?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -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<IList<LocalAccount>> {