mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-01-24 14:31:02 -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 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
import '../entities/local_account.dart';
|
import '../entities/local_account.dart';
|
||||||
import '../proto/proto.dart' as proto;
|
|
||||||
import '../entities/user_login.dart';
|
import '../entities/user_login.dart';
|
||||||
|
import '../proto/proto.dart' as proto;
|
||||||
import '../veilid_support/veilid_support.dart';
|
import '../veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import 'local_accounts.dart';
|
import 'local_accounts.dart';
|
||||||
import 'logins.dart';
|
import 'logins.dart';
|
||||||
|
|
||||||
@ -17,22 +17,23 @@ enum AccountInfoStatus {
|
|||||||
accountReady,
|
accountReady,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
class AccountInfo {
|
class AccountInfo {
|
||||||
AccountInfo({
|
const AccountInfo({
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.active,
|
required this.active,
|
||||||
this.account,
|
this.account,
|
||||||
});
|
});
|
||||||
|
|
||||||
AccountInfoStatus status;
|
final AccountInfoStatus status;
|
||||||
bool active;
|
final bool active;
|
||||||
proto.Account? account;
|
final proto.Account? account;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<AccountInfo> fetchAccount(FetchAccountRef ref,
|
Future<AccountInfo> fetchAccountInfo(FetchAccountInfoRef ref,
|
||||||
{required TypedKey accountMasterRecordKey}) async {
|
{required TypedKey accountMasterRecordKey}) async {
|
||||||
// Get which local account we want to fetch the profile for
|
// Get which local account we want to fetch the profile for
|
||||||
final localAccount = await ref.watch(
|
final localAccount = await ref.watch(
|
||||||
@ -40,7 +41,8 @@ Future<AccountInfo> fetchAccount(FetchAccountRef ref,
|
|||||||
.future);
|
.future);
|
||||||
if (localAccount == null) {
|
if (localAccount == null) {
|
||||||
// Local account does not exist
|
// 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
|
// 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);
|
status: AccountInfoStatus.accountReady, active: active, account: account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
class ActiveAccountInfo {
|
class ActiveAccountInfo {
|
||||||
ActiveAccountInfo({
|
const ActiveAccountInfo({
|
||||||
required this.localAccount,
|
required this.localAccount,
|
||||||
required this.userLogin,
|
required this.userLogin,
|
||||||
required this.account,
|
required this.account,
|
||||||
});
|
});
|
||||||
|
//
|
||||||
|
|
||||||
LocalAccount localAccount;
|
KeyPair getConversationWriter() {
|
||||||
UserLogin userLogin;
|
final identityKey = localAccount.identityMaster.identityPublicKey;
|
||||||
proto.Account account;
|
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
|
/// Get the active account info
|
||||||
@riverpod
|
@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
|
// See if we've logged into this account or if it is locked
|
||||||
final activeUserLogin = await ref.watch(loginsProvider.future
|
final activeUserLogin = await ref.watch(loginsProvider.future
|
||||||
.select((value) async => (await value).activeUserLogin));
|
.select((value) async => (await value).activeUserLogin));
|
||||||
|
@ -6,7 +6,7 @@ part of 'account.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$fetchAccountHash() => r'f3072fdd89611b53cd9821613acab450b3c08820';
|
String _$fetchAccountInfoHash() => r'3d2e3b3ddce5158d03bceaf82cdb35bae000280c';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@ -32,36 +32,36 @@ class _SystemHash {
|
|||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
@ProviderFor(fetchAccount)
|
@ProviderFor(fetchAccountInfo)
|
||||||
const fetchAccountProvider = FetchAccountFamily();
|
const fetchAccountInfoProvider = FetchAccountInfoFamily();
|
||||||
|
|
||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
class FetchAccountFamily extends Family<AsyncValue<AccountInfo>> {
|
class FetchAccountInfoFamily extends Family<AsyncValue<AccountInfo>> {
|
||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
const FetchAccountFamily();
|
const FetchAccountInfoFamily();
|
||||||
|
|
||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
FetchAccountProvider call({
|
FetchAccountInfoProvider call({
|
||||||
required Typed<FixedEncodedString43> accountMasterRecordKey,
|
required Typed<FixedEncodedString43> accountMasterRecordKey,
|
||||||
}) {
|
}) {
|
||||||
return FetchAccountProvider(
|
return FetchAccountInfoProvider(
|
||||||
accountMasterRecordKey: accountMasterRecordKey,
|
accountMasterRecordKey: accountMasterRecordKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FetchAccountProvider getProviderOverride(
|
FetchAccountInfoProvider getProviderOverride(
|
||||||
covariant FetchAccountProvider provider,
|
covariant FetchAccountInfoProvider provider,
|
||||||
) {
|
) {
|
||||||
return call(
|
return call(
|
||||||
accountMasterRecordKey: provider.accountMasterRecordKey,
|
accountMasterRecordKey: provider.accountMasterRecordKey,
|
||||||
@ -80,38 +80,38 @@ class FetchAccountFamily extends Family<AsyncValue<AccountInfo>> {
|
|||||||
_allTransitiveDependencies;
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
@override
|
@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
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
class FetchAccountProvider extends AutoDisposeFutureProvider<AccountInfo> {
|
class FetchAccountInfoProvider extends AutoDisposeFutureProvider<AccountInfo> {
|
||||||
/// Get an account from the identity key and if it is logged in and we
|
/// Get an account from the identity key and if it is logged in and we
|
||||||
/// have its secret available, return the account record contents
|
/// have its secret available, return the account record contents
|
||||||
///
|
///
|
||||||
/// Copied from [fetchAccount].
|
/// Copied from [fetchAccountInfo].
|
||||||
FetchAccountProvider({
|
FetchAccountInfoProvider({
|
||||||
required Typed<FixedEncodedString43> accountMasterRecordKey,
|
required Typed<FixedEncodedString43> accountMasterRecordKey,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
(ref) => fetchAccount(
|
(ref) => fetchAccountInfo(
|
||||||
ref as FetchAccountRef,
|
ref as FetchAccountInfoRef,
|
||||||
accountMasterRecordKey: accountMasterRecordKey,
|
accountMasterRecordKey: accountMasterRecordKey,
|
||||||
),
|
),
|
||||||
from: fetchAccountProvider,
|
from: fetchAccountInfoProvider,
|
||||||
name: r'fetchAccountProvider',
|
name: r'fetchAccountInfoProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$fetchAccountHash,
|
: _$fetchAccountInfoHash,
|
||||||
dependencies: FetchAccountFamily._dependencies,
|
dependencies: FetchAccountInfoFamily._dependencies,
|
||||||
allTransitiveDependencies:
|
allTransitiveDependencies:
|
||||||
FetchAccountFamily._allTransitiveDependencies,
|
FetchAccountInfoFamily._allTransitiveDependencies,
|
||||||
accountMasterRecordKey: accountMasterRecordKey,
|
accountMasterRecordKey: accountMasterRecordKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
FetchAccountProvider._internal(
|
FetchAccountInfoProvider._internal(
|
||||||
super._createNotifier, {
|
super._createNotifier, {
|
||||||
required super.name,
|
required super.name,
|
||||||
required super.dependencies,
|
required super.dependencies,
|
||||||
@ -125,12 +125,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider<AccountInfo> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Override overrideWith(
|
Override overrideWith(
|
||||||
FutureOr<AccountInfo> Function(FetchAccountRef provider) create,
|
FutureOr<AccountInfo> Function(FetchAccountInfoRef provider) create,
|
||||||
) {
|
) {
|
||||||
return ProviderOverride(
|
return ProviderOverride(
|
||||||
origin: this,
|
origin: this,
|
||||||
override: FetchAccountProvider._internal(
|
override: FetchAccountInfoProvider._internal(
|
||||||
(ref) => create(ref as FetchAccountRef),
|
(ref) => create(ref as FetchAccountInfoRef),
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
@ -143,12 +143,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider<AccountInfo> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
AutoDisposeFutureProviderElement<AccountInfo> createElement() {
|
AutoDisposeFutureProviderElement<AccountInfo> createElement() {
|
||||||
return _FetchAccountProviderElement(this);
|
return _FetchAccountInfoProviderElement(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return other is FetchAccountProvider &&
|
return other is FetchAccountInfoProvider &&
|
||||||
other.accountMasterRecordKey == accountMasterRecordKey;
|
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.
|
/// The parameter `accountMasterRecordKey` of this provider.
|
||||||
Typed<FixedEncodedString43> get accountMasterRecordKey;
|
Typed<FixedEncodedString43> get accountMasterRecordKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FetchAccountProviderElement
|
class _FetchAccountInfoProviderElement
|
||||||
extends AutoDisposeFutureProviderElement<AccountInfo> with FetchAccountRef {
|
extends AutoDisposeFutureProviderElement<AccountInfo>
|
||||||
_FetchAccountProviderElement(super.provider);
|
with FetchAccountInfoRef {
|
||||||
|
_FetchAccountInfoProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Typed<FixedEncodedString43> get accountMasterRecordKey =>
|
Typed<FixedEncodedString43> get accountMasterRecordKey =>
|
||||||
(origin as FetchAccountProvider).accountMasterRecordKey;
|
(origin as FetchAccountInfoProvider).accountMasterRecordKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$fetchActiveAccountHash() =>
|
String _$fetchActiveAccountInfoHash() =>
|
||||||
r'197e5dd793563ff1d9927309a5ec9db1c9f67f07';
|
r'85276ff85b0e82c8d3c6313250954f5b578697d1';
|
||||||
|
|
||||||
/// Get the active account info
|
/// Get the active account info
|
||||||
///
|
///
|
||||||
/// Copied from [fetchActiveAccount].
|
/// Copied from [fetchActiveAccountInfo].
|
||||||
@ProviderFor(fetchActiveAccount)
|
@ProviderFor(fetchActiveAccountInfo)
|
||||||
final fetchActiveAccountProvider =
|
final fetchActiveAccountInfoProvider =
|
||||||
AutoDisposeFutureProvider<ActiveAccountInfo?>.internal(
|
AutoDisposeFutureProvider<ActiveAccountInfo?>.internal(
|
||||||
fetchActiveAccount,
|
fetchActiveAccountInfo,
|
||||||
name: r'fetchActiveAccountProvider',
|
name: r'fetchActiveAccountInfoProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$fetchActiveAccountHash,
|
: _$fetchActiveAccountInfoHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef FetchActiveAccountRef
|
typedef FetchActiveAccountInfoRef
|
||||||
= AutoDisposeFutureProviderRef<ActiveAccountInfo?>;
|
= AutoDisposeFutureProviderRef<ActiveAccountInfo?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// 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
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$fetchChatListHash() => r'407692f9d6794a5a2b356d7a34240624b211daa8';
|
String _$fetchChatListHash() => r'0c166082625799862128dff09d9286f64785ba6c';
|
||||||
|
|
||||||
/// Get the active account contact list
|
/// Get the active account contact list
|
||||||
///
|
///
|
||||||
/// Copied from [fetchChatList].
|
/// Copied from [fetchChatList].
|
||||||
@ProviderFor(fetchChatList)
|
@ProviderFor(fetchChatList)
|
||||||
final fetchChatListProvider = AutoDisposeFutureProvider<IList<Chat>?>.internal(
|
final fetchChatListProvider =
|
||||||
|
AutoDisposeFutureProvider<IList<proto.Chat>?>.internal(
|
||||||
fetchChatList,
|
fetchChatList,
|
||||||
name: r'fetchChatListProvider',
|
name: r'fetchChatListProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@ -22,6 +23,6 @@ final fetchChatListProvider = AutoDisposeFutureProvider<IList<Chat>?>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef FetchChatListRef = AutoDisposeFutureProviderRef<IList<Chat>?>;
|
typedef FetchChatListRef = AutoDisposeFutureProviderRef<IList<proto.Chat>?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// 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';
|
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
|
/// Get the active account contact invitation list
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(
|
Future<IList<proto.ContactInvitationRecord>?> fetchContactInvitationRecords(
|
||||||
|
@ -7,14 +7,14 @@ part of 'contact_invite.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$fetchContactInvitationRecordsHash() =>
|
String _$fetchContactInvitationRecordsHash() =>
|
||||||
r'365d563c5e66f45679f597502ea9e4b8296ff8af';
|
r'ff0b2c68d42cb106602982b1fb56a7bd8183c04a';
|
||||||
|
|
||||||
/// Get the active account contact invitation list
|
/// Get the active account contact invitation list
|
||||||
///
|
///
|
||||||
/// Copied from [fetchContactInvitationRecords].
|
/// Copied from [fetchContactInvitationRecords].
|
||||||
@ProviderFor(fetchContactInvitationRecords)
|
@ProviderFor(fetchContactInvitationRecords)
|
||||||
final fetchContactInvitationRecordsProvider =
|
final fetchContactInvitationRecordsProvider =
|
||||||
AutoDisposeFutureProvider<IList<ContactInvitationRecord>?>.internal(
|
AutoDisposeFutureProvider<IList<proto.ContactInvitationRecord>?>.internal(
|
||||||
fetchContactInvitationRecords,
|
fetchContactInvitationRecords,
|
||||||
name: r'fetchContactInvitationRecordsProvider',
|
name: r'fetchContactInvitationRecordsProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@ -25,6 +25,6 @@ final fetchContactInvitationRecordsProvider =
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef FetchContactInvitationRecordsRef
|
typedef FetchContactInvitationRecordsRef
|
||||||
= AutoDisposeFutureProviderRef<IList<ContactInvitationRecord>?>;
|
= AutoDisposeFutureProviderRef<IList<proto.ContactInvitationRecord>?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// 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 'dart:convert';
|
||||||
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
@ -14,25 +18,230 @@ import 'contact.dart';
|
|||||||
|
|
||||||
part 'conversation.g.dart';
|
part 'conversation.g.dart';
|
||||||
|
|
||||||
Future<DHTRecordCrypto> getConversationCrypto({
|
class Conversation {
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
Conversation._(
|
||||||
|
{required ActiveAccountInfo activeAccountInfo,
|
||||||
|
required TypedKey localConversationRecordKey,
|
||||||
required TypedKey remoteIdentityPublicKey,
|
required TypedKey remoteIdentityPublicKey,
|
||||||
}) async {
|
required TypedKey remoteConversationRecordKey})
|
||||||
|
: _activeAccountInfo = activeAccountInfo,
|
||||||
|
_localConversationRecordKey = localConversationRecordKey,
|
||||||
|
_remoteIdentityPublicKey = remoteIdentityPublicKey,
|
||||||
|
_remoteConversationRecordKey = remoteConversationRecordKey;
|
||||||
|
|
||||||
|
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 veilid = await eventualVeilid.future;
|
||||||
final identitySecret = activeAccountInfo.userLogin.identitySecret;
|
final identitySecret = _activeAccountInfo.userLogin.identitySecret;
|
||||||
final cs = await veilid.getCryptoSystem(identitySecret.kind);
|
final cs = await veilid.getCryptoSystem(identitySecret.kind);
|
||||||
final sharedSecret =
|
final sharedSecret =
|
||||||
await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value);
|
await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value);
|
||||||
return DHTRecordCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyPair getConversationWriter({
|
conversationCrypto = await DHTRecordCryptoPrivate.fromSecret(
|
||||||
required ActiveAccountInfo activeAccountInfo,
|
identitySecret.kind, sharedSecret);
|
||||||
}) {
|
_conversationCrypto = conversationCrypto;
|
||||||
final identityKey =
|
return conversationCrypto;
|
||||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
}
|
||||||
final identitySecret = activeAccountInfo.userLogin.identitySecret;
|
|
||||||
return KeyPair(key: identityKey, secret: identitySecret.value);
|
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
|
// Create a conversation
|
||||||
@ -51,7 +260,7 @@ Future<T> createConversation<T>(
|
|||||||
final crypto = await getConversationCrypto(
|
final crypto = await getConversationCrypto(
|
||||||
activeAccountInfo: activeAccountInfo,
|
activeAccountInfo: activeAccountInfo,
|
||||||
remoteIdentityPublicKey: remoteIdentityPublicKey);
|
remoteIdentityPublicKey: remoteIdentityPublicKey);
|
||||||
final writer = getConversationWriter(activeAccountInfo: activeAccountInfo);
|
final writer = activeAccountInfo.getConversationWriter();
|
||||||
|
|
||||||
// Open with SMPL scheme for identity writer
|
// Open with SMPL scheme for identity writer
|
||||||
late final DHTRecord localConversationRecord;
|
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
|
@riverpod
|
||||||
class ActiveConversationMessages extends _$ActiveConversationMessages {
|
class ActiveConversationMessages extends _$ActiveConversationMessages {
|
||||||
|
@ -7,12 +7,12 @@ part of 'conversation.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$activeConversationMessagesHash() =>
|
String _$activeConversationMessagesHash() =>
|
||||||
r'61c9e16f1304c7929a971ec7711d2b6c7cadc5ea';
|
r'5579a9386f2046b156720ae799a0e77aca119b09';
|
||||||
|
|
||||||
/// See also [ActiveConversationMessages].
|
/// See also [ActiveConversationMessages].
|
||||||
@ProviderFor(ActiveConversationMessages)
|
@ProviderFor(ActiveConversationMessages)
|
||||||
final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider<
|
final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
ActiveConversationMessages, IList<Message>?>.internal(
|
ActiveConversationMessages, IList<proto.Message>?>.internal(
|
||||||
ActiveConversationMessages.new,
|
ActiveConversationMessages.new,
|
||||||
name: r'activeConversationMessagesProvider',
|
name: r'activeConversationMessagesProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@ -23,6 +23,6 @@ final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ActiveConversationMessages
|
typedef _$ActiveConversationMessages
|
||||||
= AutoDisposeAsyncNotifier<IList<Message>?>;
|
= AutoDisposeAsyncNotifier<IList<proto.Message>?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// 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';
|
const String veilidChatAccountKey = 'com.veilid.veilidchat';
|
||||||
|
|
||||||
// Local account manager
|
// Local accounts table
|
||||||
@riverpod
|
@riverpod
|
||||||
class LocalAccounts extends _$LocalAccounts
|
class LocalAccounts extends _$LocalAccounts
|
||||||
with AsyncTableDBBacked<IList<LocalAccount>> {
|
with AsyncTableDBBacked<IList<LocalAccount>> {
|
||||||
|
Loading…
Reference in New Issue
Block a user