From 6f525843ff3c9f6130e3bfc13e140a05d6951df5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 5 Aug 2023 21:01:27 -0400 Subject: [PATCH] contact accept --- assets/i18n/en.json | 4 +- doc/invitations.md | 6 +- lib/components/paste_invite_dialog.dart | 27 +++++- lib/providers/contact.dart | 118 +++++++++++++++++++----- 4 files changed, 125 insertions(+), 30 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index dc03130..90e40a9 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -88,7 +88,9 @@ "paste": "Paste", "message_from_contact": "Message from contact", "validating": "Validating...", - "invalid_invitation": "Invalid invitation" + "invalid_invitation": "Invalid invitation", + "failed_to_accept": "Failed to accept contact invite", + "failed_to_reject": "Failed to reject contact invite" }, "enter_pin_dialog": { "enter_pin": "Enter PIN", diff --git a/doc/invitations.md b/doc/invitations.md index b448d85..a914dc3 100644 --- a/doc/invitations.md +++ b/doc/invitations.md @@ -34,7 +34,7 @@ 3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret ## Receiving an accept/reject -1. Open and get SignedContactResponse from ContactRequest unicaseinbox DHT record +1. Open and get SignedContactResponse from ContactRequest unicastinbox DHT record 2. Decrypt with writer secret 3. Get DHT record for contact's AccountMaster 4. Validate the SignedContactResponse signature @@ -42,8 +42,10 @@ If accept == false: 1. Announce rejection 2. Delete local invitation from table + 3. Overwrite and delete ContactRequest inbox If accept == true: 1. Add a local contact with the remote chat dht record, updating from the remote profile in it. 2. Delete local invitation from table - + 3. Overwrite and delete ContactRequest inbox + diff --git a/lib/components/paste_invite_dialog.dart b/lib/components/paste_invite_dialog.dart index a045ff4..a673a2c 100644 --- a/lib/components/paste_invite_dialog.dart +++ b/lib/components/paste_invite_dialog.dart @@ -111,7 +111,24 @@ class PasteInviteDialogState extends ConsumerState { } final validInvitation = _validInvitation; if (validInvitation != null) { - await acceptContactInvitation(activeAccountInfo, validInvitation); + final acceptedContact = + await acceptContactInvitation(activeAccountInfo, validInvitation); + if (acceptedContact != null) { + await createContact( + activeAccountInfo: activeAccountInfo, + profile: acceptedContact.profile, + remoteIdentity: acceptedContact.remoteIdentity, + remoteConversation: acceptedContact.remoteConversation, + localConversation: acceptedContact.localConversation, + ); + ref + ..invalidate(fetchContactInvitationRecordsProvider) + ..invalidate(fetchContactListProvider); + } else { + if (context.mounted) { + showErrorToast(context, 'paste_invite_dialog.failed_to_accept'); + } + } } setState(() { _isAccepting = false; @@ -135,7 +152,13 @@ class PasteInviteDialogState extends ConsumerState { } final validInvitation = _validInvitation; if (validInvitation != null) { - await rejectContactInvitation(activeAccountInfo, validInvitation); + if (await rejectContactInvitation(activeAccountInfo, validInvitation)) { + // do nothing right now + } else { + if (context.mounted) { + showErrorToast(context, 'paste_invite_dialog.failed_to_reject'); + } + } } setState(() { _isAccepting = false; diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart index 5b89140..b1065f4 100644 --- a/lib/providers/contact.dart +++ b/lib/providers/contact.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -330,46 +331,79 @@ Future validateContactInvitation(Uint8List inviteData, return out; } -Future acceptContactInvitation(ActiveAccountInfo activeAccountInfo, +class AcceptedContact { + AcceptedContact({ + required this.profile, + required this.remoteIdentity, + required this.remoteConversation, + required this.localConversation, + }); + + proto.Profile profile; + IdentityMaster remoteIdentity; + TypedKey remoteConversation; + OwnedDHTRecordPointer localConversation; +} + +Future acceptContactInvitation( + ActiveAccountInfo activeAccountInfo, ValidContactInvitation validContactInvitation) async { final pool = await DHTRecordPool.instance(); - await (await pool.openWrite(validContactInvitation.contactRequestInboxKey, + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, validContactInvitation.writer)) .deleteScope((contactRequestInbox) async { final cs = await pool.veilid .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); - // xxx - final contactResponse = ContactResponse() - ..accept = false - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); + // Create local conversation key for this + // contact and send via contact response + return (await pool.create(parent: accountRecordKey)) + .deleteScope((localConversation) async { + final contactResponse = ContactResponse() + ..accept = true + ..remoteConversationKey = localConversation.key.toProto() + ..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 identitySignature = await cs.sign( + activeAccountInfo.localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); - final signedContactResponse = SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); + final signedContactResponse = SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); - // Write the rejection to the invox - if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to accept contact invitation'); - } + // Write the acceptance to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + SignedContactResponse.fromBuffer, signedContactResponse, + subkey: 1) != + null) { + log.error('failed to accept contact invitation'); + await localConversation.delete(); + await contactRequestInbox.delete(); + return null; + } + return AcceptedContact( + profile: validContactInvitation.contactRequestPrivate.profile, + remoteIdentity: validContactInvitation.contactIdentityMaster, + remoteConversation: proto.TypedKeyProto.fromProto( + validContactInvitation.contactRequestPrivate.chatRecordKey), + localConversation: localConversation.ownedDHTRecordPointer, + ); + }); }); } -Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, +Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, ValidContactInvitation validContactInvitation) async { final pool = await DHTRecordPool.instance(); - await (await pool.openWrite(validContactInvitation.contactRequestInboxKey, + return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, validContactInvitation.writer)) .deleteScope((contactRequestInbox) async { final cs = await pool.veilid @@ -397,6 +431,40 @@ Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, subkey: 1) != null) { log.error('failed to reject contact invitation'); + return false; + } + return true; + }); +} + +Future createContact({ + required ActiveAccountInfo activeAccountInfo, + required proto.Profile profile, + required IdentityMaster remoteIdentity, + required TypedKey remoteConversation, + required OwnedDHTRecordPointer localConversation, +}) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + // Create Contact + final contact = Contact() + ..editedProfile = profile + ..remoteProfile = profile + ..remoteIdentity = jsonEncode(remoteIdentity.toJson()) + ..remoteConversationKey = remoteConversation.toProto() + ..localConversation = localConversation.toProto() + ..showAvailability = false; + + // Add Contact 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.contactList), + parent: accountRecordKey)) + .scope((contactList) async { + if (await contactList.tryAddItem(contact.writeToBuffer()) == false) { + throw StateError('Failed to add contact'); } }); }