From 29cafb11ebde270cd5888459b4801a03839d780e Mon Sep 17 00:00:00 2001 From: Clark Date: Thu, 23 May 2024 13:06:08 -0400 Subject: [PATCH] Update proto and add payments export without tombstone. --- .../v2/database/ChatItemExportIterator.kt | 69 ++++++++++++++++++- .../v2/database/ChatItemImportInserter.kt | 13 +++- .../model/GroupCallUpdateDetailsUtil.java | 8 +-- app/src/main/protowire/Backup.proto | 54 ++++++++++++++- 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt index 8ba159bae6..02daf434d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.GroupCall import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment +import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.Quote import org.thoughtcrime.securesms.backup.v2.proto.Reaction @@ -41,8 +42,10 @@ import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.GroupReceiptTable import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.PaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet import org.thoughtcrime.securesms.database.documents.NetworkFailureSet import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil @@ -57,6 +60,9 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchove import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.QuoteModel +import org.thoughtcrime.securesms.payments.FailureReason +import org.thoughtcrime.securesms.payments.State +import org.thoughtcrime.securesms.payments.proto.PaymentMetaData import org.thoughtcrime.securesms.util.JsonUtils import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.util.UuidUtil @@ -65,6 +71,7 @@ import java.io.Closeable import java.io.IOException import java.util.LinkedList import java.util.Queue +import kotlin.jvm.optionals.getOrNull import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange /** @@ -219,7 +226,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING }, ringerRecipientId = call.ringerRecipient?.toLong(), - startedCallAci = if (call.ringerRecipient != null) SignalDatabase.recipients.getRecord(call.ringerRecipient).aci?.toByteString() else null, + startedCallRecipientId = call.ringerRecipient?.toLong(), startedCallTimestamp = call.timestamp ) ) @@ -304,7 +311,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: builder.updateMessage = ChatUpdateMessage( groupCall = GroupCall( state = GroupCall.State.GENERIC, - startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(), + startedCallRecipientId = recipients.getByAci(ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid))).getOrNull()?.toLong(), startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp, endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp ) @@ -316,6 +323,24 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: } } } + MessageTypes.isPaymentsNotification(record.type) -> { + val paymentUuid = UuidUtil.parseOrNull(record.body) + val payment = if (paymentUuid != null) { + SignalDatabase.payments.getPayment(paymentUuid) + } else { + null + } + if (payment == null) { + builder.paymentNotification = PaymentNotification() + } else { + builder.paymentNotification = PaymentNotification( + amountMob = payment.amount.serializeAmountString(), + feeMob = payment.fee.serializeAmountString(), + note = payment.note, + transactionDetails = payment.getTransactionDetails() + ) + } + } record.body == null && !attachmentsById.containsKey(record.id) -> { Log.w(TAG, "Record missing a body and doesnt have attachments, skipping") continue @@ -484,6 +509,46 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: } } + private fun PaymentTable.PaymentTransaction.getTransactionDetails(): PaymentNotification.TransactionDetails? { + if (failureReason != null || state == State.FAILED) { + return PaymentNotification.TransactionDetails(failedTransaction = PaymentNotification.TransactionDetails.FailedTransaction(reason = failureReason.toBackupFailureReason())) + } + return PaymentNotification.TransactionDetails( + transaction = PaymentNotification.TransactionDetails.Transaction( + status = this.state.toBackupState(), + timestamp = timestamp, + blockIndex = blockIndex, + blockTimestamp = blockTimestamp, + mobileCoinIdentification = paymentMetaData.mobileCoinTxoIdentification?.toBackup() + ) + ) + } + + private fun PaymentMetaData.MobileCoinTxoIdentification.toBackup(): PaymentNotification.TransactionDetails.MobileCoinTxoIdentification { + return PaymentNotification.TransactionDetails.MobileCoinTxoIdentification( + publicKey = this.publicKey, + keyImages = this.keyImages + ) + } + + private fun State.toBackupState(): PaymentNotification.TransactionDetails.Transaction.Status { + return when (this) { + State.INITIAL -> PaymentNotification.TransactionDetails.Transaction.Status.INITIAL + State.SUBMITTED -> PaymentNotification.TransactionDetails.Transaction.Status.SUBMITTED + State.SUCCESSFUL -> PaymentNotification.TransactionDetails.Transaction.Status.SUCCESSFUL + State.FAILED -> throw IllegalArgumentException("state cannot be failed") + } + } + + private fun FailureReason?.toBackupFailureReason(): PaymentNotification.TransactionDetails.FailedTransaction.FailureReason { + return when (this) { + FailureReason.UNKNOWN -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC + FailureReason.INSUFFICIENT_FUNDS -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.INSUFFICIENT_FUNDS + FailureReason.NETWORK -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.NETWORK + else -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC + } + } + private fun List.toBackupBodyRanges(): List { return this.map { BackupBodyRange( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt index b1116f8dc6..abbab849e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.MessageTypes import org.thoughtcrime.securesms.database.ReactionTable import org.thoughtcrime.securesms.database.SQLiteDatabase import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet import org.thoughtcrime.securesms.database.documents.NetworkFailure @@ -500,7 +501,17 @@ class ChatItemImportInserter( this.put(MessageTable.TYPE, typeFlags) } updateMessage.groupCall != null -> { - this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall)) + val startedCallRecipientId = if (updateMessage.groupCall.startedCallRecipientId != null) { + backupState.backupToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId] + } else { + null + } + val startedCall = if (startedCallRecipientId != null) { + recipients.getRecord(startedCallRecipientId).aci + } else { + null + } + this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall, startedCall)) this.put(MessageTable.TYPE, MessageTypes.GROUP_CALL_TYPE) } updateMessage.groupChange != null -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java index 2264893a89..dfae412e4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java @@ -6,8 +6,10 @@ import androidx.annotation.Nullable; import org.signal.core.util.Base64; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.backup.v2.proto.GroupCall; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.push.ServiceId; import java.io.IOException; @@ -28,11 +30,9 @@ public final class GroupCallUpdateDetailsUtil { /** * Generates a group chat update message body from backup data */ - public static @NonNull String createBodyFromBackup(@NonNull GroupCall groupCallChatUpdate) { - ServiceId.ACI startedCall = groupCallChatUpdate.startedCallAci != null ? ServiceId.ACI.parseOrNull(groupCallChatUpdate.startedCallAci) : null; - + public static @NonNull String createBodyFromBackup(@NonNull GroupCall groupCallChatUpdate, ServiceId.ACI startedCallAci) { GroupCallUpdateDetails details = new GroupCallUpdateDetails.Builder() - .startedCallUuid(Objects.toString(startedCall, null)) + .startedCallUuid(Objects.toString(startedCallAci, null)) .startedCallTimestamp(groupCallChatUpdate.startedCallTimestamp) .endedCallTimestamp(groupCallChatUpdate.endedCallTimestamp) .isCallFull(false) diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index 91d60648a3..e0d3a1882b 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -326,6 +326,7 @@ message ChatItem { StickerMessage stickerMessage = 13; RemoteDeletedMessage remoteDeletedMessage = 14; ChatUpdateMessage updateMessage = 15; + PaymentNotification paymentNotification = 16; } } @@ -368,6 +369,57 @@ message ContactMessage { repeated Reaction reactions = 2; } +message PaymentNotification { + + // Transaction details should largely be best effort. If fields are missing or + // invalid, we should try to render things to the best of our abilities. + message TransactionDetails { + message MobileCoinTxoIdentification { // Used to map to payments on the ledger + repeated bytes publicKey = 1; // for received transactions + repeated bytes keyImages = 2; // for sent transactions + } + + message FailedTransaction { // Failed payments can't be synced from the ledger + enum FailureReason { + GENERIC = 0; + NETWORK = 1; + INSUFFICIENT_FUNDS = 2; + } + FailureReason reason = 1; + } + + message Transaction { + enum Status { + INITIAL = 0; + SUBMITTED = 1; + SUCCESSFUL = 2; + } + Status status = 1; + + // This identification is used to map the payment table to the ledger + // and is likely required otherwise we may have issues reconciling with + // the ledger. If this + MobileCoinTxoIdentification mobileCoinIdentification = 2; + optional uint64 timestamp = 3; + optional uint64 blockIndex = 4; + optional uint64 blockTimestamp = 5; + optional bytes transaction = 6; // mobile coin blobs, these are best effort, if they are invalid or fail to parse, treat as if missing, but do not block + optional bytes receipt = 7; // mobile coin blobs, these are best effort, if they are invalid or fail to parse, treat as if missing, but do not block + } + + oneof payment { + Transaction transaction = 1; + FailedTransaction failedTransaction = 2; + } + } + + optional string amountMob = 1; // stored as a decimal string, e.g. 1.00001 + optional string feeMob = 2; // stored as a decimal string, e.g. 1.00001 + optional string note = 3; + TransactionDetails transactionDetails = 4; + +} + message ContactAttachment { message Name { optional string givenName = 1; @@ -656,7 +708,7 @@ message GroupCall { optional uint64 callId = 1; State state = 2; optional uint64 ringerRecipientId = 3; - optional bytes startedCallAci = 4; + optional uint64 startedCallRecipientId = 4; uint64 startedCallTimestamp = 5; // The time the call ended. 0 indicates an unknown time. uint64 endedCallTimestamp = 6;