mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-01-23 22:11:19 -05:00
refactoring
This commit is contained in:
parent
d3ecae0113
commit
e898074387
0
lib/managers/contact_list_manager.dart
Normal file
0
lib/managers/contact_list_manager.dart
Normal file
1
lib/managers/valid_contact_invitation.dart
Normal file
1
lib/managers/valid_contact_invitation.dart
Normal file
@ -0,0 +1 @@
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
583
lib/providers/contact_invitation_list_manager.dart
Normal file
583
lib/providers/contact_invitation_list_manager.dart
Normal 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;
|
||||
}
|
202
lib/providers/contact_invitation_list_manager.g.dart
Normal file
202
lib/providers/contact_invitation_list_manager.g.dart
Normal 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
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
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<Conversation> open() async {}
|
||||
|
||||
Future<void> close() async {
|
||||
//
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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
|
||||
|
@ -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>> {
|
||||
|
Loading…
Reference in New Issue
Block a user