From 754d759d7d6089fffdc06f6789580dcd60c89650 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 13 Feb 2025 11:46:09 -0500 Subject: [PATCH] Add support for AttachmentBackfill sync messages. --- .../securesms/testing/MessageContentFuzzer.kt | 26 +- .../attachments/AttachmentExtensions.kt | 96 ++++++ .../ConversationListFragment.java | 2 + .../securesms/database/MessageTable.kt | 9 + .../securesms/jobmanager/Job.java | 11 + .../securesms/jobmanager/JobController.java | 5 +- .../securesms/jobmanager/JobManager.java | 12 - .../securesms/jobs/AttachmentUploadJob.kt | 1 - .../securesms/jobs/JobManagerFactories.java | 302 +++++++++--------- ...MultiDeviceAttachmentBackfillMissingJob.kt | 102 ++++++ .../MultiDeviceAttachmentBackfillUpdateJob.kt | 149 +++++++++ .../jobs/MultiDeviceDeleteSyncJob.kt | 29 +- .../messages/SyncMessageProcessor.kt | 76 ++++- .../securesms/util/RemoteConfig.kt | 7 + app/src/main/protowire/JobData.proto | 14 +- .../api/SignalServiceMessageSender.java | 9 + .../multidevice/SignalServiceSyncMessage.java | 91 ++++-- .../src/main/protowire/SignalService.proto | 69 +++- 18 files changed, 781 insertions(+), 229 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentExtensions.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillMissingJob.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillUpdateJob.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt index 2b8610d281..c5b6c4dda5 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt @@ -11,9 +11,11 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.api.util.UuidUtil +import org.whispersystems.signalservice.internal.push.AddressableMessage import org.whispersystems.signalservice.internal.push.AttachmentPointer import org.whispersystems.signalservice.internal.push.BodyRange import org.whispersystems.signalservice.internal.push.Content +import org.whispersystems.signalservice.internal.push.ConversationIdentifier import org.whispersystems.signalservice.internal.push.DataMessage import org.whispersystems.signalservice.internal.push.EditMessage import org.whispersystems.signalservice.internal.push.Envelope @@ -163,13 +165,13 @@ object MessageContentFuzzer { val conversation = Recipient.resolved(conversationId) SyncMessage.DeleteForMe.MessageDeletes( conversation = if (conversation.isGroup) { - SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) + ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) + ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) }, messages = conversationDeletes.map { (author, timestamp) -> - SyncMessage.DeleteForMe.AddressableMessage( + AddressableMessage( authorServiceId = Recipient.resolved(author).requireAci().toString(), sentTimestamp = timestamp ) @@ -191,20 +193,20 @@ object MessageContentFuzzer { val conversation = Recipient.resolved(delete.conversationId) SyncMessage.DeleteForMe.ConversationDelete( conversation = if (conversation.isGroup) { - SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) + ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) + ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) }, mostRecentMessages = delete.messages.map { (author, timestamp) -> - SyncMessage.DeleteForMe.AddressableMessage( + AddressableMessage( authorServiceId = Recipient.resolved(author).requireAci().toString(), sentTimestamp = timestamp ) }, mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) -> - SyncMessage.DeleteForMe.AddressableMessage( + AddressableMessage( authorServiceId = Recipient.resolved(author).requireAci().toString(), sentTimestamp = timestamp ) @@ -228,9 +230,9 @@ object MessageContentFuzzer { val conversation = Recipient.resolved(conversationId) SyncMessage.DeleteForMe.LocalOnlyConversationDelete( conversation = if (conversation.isGroup) { - SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) + ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) + ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) } ) } @@ -250,11 +252,11 @@ object MessageContentFuzzer { attachmentDeletes = listOf( SyncMessage.DeleteForMe.AttachmentDelete( conversation = if (conversation.isGroup) { - SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) + ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) + ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) }, - targetMessage = SyncMessage.DeleteForMe.AddressableMessage( + targetMessage = AddressableMessage( authorServiceId = Recipient.resolved(message.first).requireAci().toString(), sentTimestamp = message.second ), diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentExtensions.kt new file mode 100644 index 0000000000..c3b1ade837 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentExtensions.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.attachments + +import android.content.Context +import android.text.TextUtils +import okio.ByteString.Companion.toByteString +import org.signal.core.util.Base64 +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId +import org.whispersystems.signalservice.api.util.toByteArray +import org.whispersystems.signalservice.internal.push.AttachmentPointer +import java.io.IOException + +/** + * Converts an [Attachment] to an [AttachmentPointer]. Will return null if any essential data is invalid. + */ +fun Attachment.toAttachmentPointer(context: Context): AttachmentPointer? { + val attachment = this + + if (TextUtils.isEmpty(attachment.remoteLocation)) { + return null + } + + if (TextUtils.isEmpty(attachment.remoteKey)) { + return null + } + + try { + val remoteId = SignalServiceAttachmentRemoteId.from(attachment.remoteLocation!!) + + var attachmentWidth = attachment.width + var attachmentHeight = attachment.height + + if ((attachmentWidth == 0 || attachmentHeight == 0) && MediaUtil.hasVideoThumbnail(context, attachment.uri)) { + val thumbnail = MediaUtil.getVideoThumbnail(context, attachment.uri, 1000) + + if (thumbnail != null) { + attachmentWidth = thumbnail.width + attachmentHeight = thumbnail.height + } + } + + return AttachmentPointer.Builder().apply { + cdnNumber = attachment.cdn.cdnNumber + contentType = attachment.contentType + key = Base64.decode(attachment.remoteKey!!).toByteString() + digest = attachment.remoteDigest?.toByteString() + size = Util.toIntExact(attachment.size) + uploadTimestamp = attachment.uploadTimestamp + width = attachmentWidth.takeIf { it > 0 } + height = attachmentHeight.takeIf { it > 0 } + fileName = attachment.fileName + incrementalMac = attachment.incrementalDigest?.toByteString() + chunkSize = attachment.incrementalMacChunkSize.takeIf { it > 0 } + flags = attachment.toFlags() + caption = attachment.caption + blurHash = attachment.blurHash?.hash + clientUuid = attachment.uuid?.toByteArray()?.toByteString() + + if (remoteId is SignalServiceAttachmentRemoteId.V2) { + cdnId = remoteId.cdnId + } + + if (remoteId is SignalServiceAttachmentRemoteId.V4) { + cdnKey = remoteId.cdnKey + } + }.build() + } catch (e: IOException) { + return null + } catch (e: ArithmeticException) { + return null + } +} + +private fun Attachment.toFlags(): Int { + var flags = 0 + + if (this.voiceNote) { + flags = flags or AttachmentPointer.Flags.VOICE_MESSAGE.value + } + + if (this.borderless) { + flags = flags or AttachmentPointer.Flags.BORDERLESS.value + } + + if (this.videoGif) { + flags = flags or AttachmentPointer.Flags.GIF.value + } + + return flags +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index ca84e973f6..71671bb411 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -145,6 +145,8 @@ import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.groups.SelectionLimits; +import org.thoughtcrime.securesms.jobs.MultiDeviceAttachmentBackfillMissingJob; +import org.thoughtcrime.securesms.jobs.MultiDeviceAttachmentBackfillUpdateJob; import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; import org.thoughtcrime.securesms.keyvalue.AccountValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 1555522698..f794096632 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -3570,6 +3570,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat .readToSingleLongOrNull() } + fun getMessageIdOrNull(message: SyncMessageId, threadId: Long): Long? { + return readableDatabase + .select(ID) + .from(TABLE_NAME) + .where("$DATE_SENT = ? AND $FROM_RECIPIENT_ID = ? AND $THREAD_ID = $threadId", message.timetamp, message.recipientId) + .run() + .readToSingleLongOrNull() + } + fun deleteMessages(messagesToDelete: List): List { val threads = mutableSetOf() val unhandled = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java index 657026c4e1..c81b765989 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java @@ -42,6 +42,7 @@ public abstract class Job { private long lastRunAttemptTime; private long nextBackoffInterval; + private volatile boolean cascadingFailure; private volatile boolean canceled; protected Context context; @@ -106,6 +107,16 @@ public abstract class Job { this.canceled = true; } + /** Indicates that this job is failing because a job earlier in the chain failed. */ + final void markCascadingFailure() { + this.cascadingFailure = true; + } + + /** Whether or not this job is failing because a job earlier in the chain failed. */ + protected boolean isCascadingFailure() { + return this.cascadingFailure; + } + /** Provides a default exponential backoff given the current run attempt. */ protected final long defaultBackoff() { return BackoffUtil.exponentialBackoff(runAttempt + 1, RemoteConfig.getDefaultMaxBackoff()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java index df73b710c6..3935f9150e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobController.java @@ -145,7 +145,10 @@ class JobController { List dependents = onFailure(job); job.setContext(application); job.onFailure(); - Stream.of(dependents).forEach(Job::onFailure); + for (Job child : dependents) { + child.markCascadingFailure(); + child.onFailure(); + } return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java index c75ca04690..3be335af51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -535,18 +535,6 @@ public class JobManager implements ConstraintObserver.Notifier { return this; } - public Chain after(@NonNull Job job) { - return after(Collections.singletonList(job)); - } - - public Chain after(@NonNull List jobs) { - if (!jobs.isEmpty()) { - this.jobs.add(0, new ArrayList<>(jobs)); - } - - return this; - } - public void enqueue() { jobManager.enqueueChain(this); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt index 10ea49fc4e..d4fcf01a35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -138,7 +138,6 @@ class AttachmentUploadJob private constructor( SignalDatabase.attachments.createKeyIvIfNecessary(attachmentId) - val messageSender = AppDependencies.signalServiceMessageSender val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId) ?: throw InvalidAttachmentException("Cannot find the specified attachment.") val timeSinceUpload = System.currentTimeMillis() - databaseAttachment.uploadTimestamp diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 7c9de0afc2..053464addf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -113,156 +113,158 @@ import java.util.Map; public final class JobManagerFactories { public static Map getJobFactories(@NonNull Application application) { - return new HashMap() {{ - put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory()); - put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory()); - put(ApkUpdateJob.KEY, new ApkUpdateJob.Factory()); - put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory()); - put(ArchiveThumbnailUploadJob.KEY, new ArchiveThumbnailUploadJob.Factory()); - put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); - put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory()); - put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); - put(AttachmentHashBackfillJob.KEY, new AttachmentHashBackfillJob.Factory()); - put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); - put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory()); - put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); - put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); - put(BackfillDigestJob.KEY, new BackfillDigestJob.Factory()); - put(BackfillDigestsForDataFileJob.KEY, new BackfillDigestsForDataFileJob.Factory()); - put(BackupMessagesJob.KEY, new BackupMessagesJob.Factory()); - put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory()); - put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory()); - put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory()); - put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory()); - put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory()); - put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory()); - put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory()); - put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory()); - put(CheckRestoreMediaLeftJob.KEY, new CheckRestoreMediaLeftJob.Factory()); - put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory()); - put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); - put(ConversationShortcutRankingUpdateJob.KEY, new ConversationShortcutRankingUpdateJob.Factory()); - put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory()); - put(CopyAttachmentToArchiveJob.KEY, new CopyAttachmentToArchiveJob.Factory()); - put(CreateReleaseChannelJob.KEY, new CreateReleaseChannelJob.Factory()); - put(DeleteAbandonedAttachmentsJob.KEY, new DeleteAbandonedAttachmentsJob.Factory()); - put(NewLinkedDeviceNotificationJob.KEY, new NewLinkedDeviceNotificationJob.Factory()); - put(DeviceNameChangeJob.KEY, new DeviceNameChangeJob.Factory()); - put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory()); - put(DownloadLatestEmojiDataJob.KEY, new DownloadLatestEmojiDataJob.Factory()); - put(EmojiSearchIndexDownloadJob.KEY, new EmojiSearchIndexDownloadJob.Factory()); - put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory()); - put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory()); - put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory()); - put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory()); - put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory()); - put(GenerateAudioWaveFormJob.KEY, new GenerateAudioWaveFormJob.Factory()); - put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory()); - put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory()); - put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory()); - put(GroupRingCleanupJob.KEY, new GroupRingCleanupJob.Factory()); - put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory()); - put(InAppPaymentAuthCheckJob.KEY, new InAppPaymentAuthCheckJob.Factory()); - put(InAppPaymentGiftSendJob.KEY, new InAppPaymentGiftSendJob.Factory()); - put(InAppPaymentKeepAliveJob.KEY, new InAppPaymentKeepAliveJob.Factory()); - put(InAppPaymentPurchaseTokenJob.KEY, new InAppPaymentPurchaseTokenJob.Factory()); - put(InAppPaymentRecurringContextJob.KEY, new InAppPaymentRecurringContextJob.Factory()); - put(InAppPaymentOneTimeContextJob.KEY, new InAppPaymentOneTimeContextJob.Factory()); - put(InAppPaymentRedemptionJob.KEY, new InAppPaymentRedemptionJob.Factory()); - put(IndividualSendJob.KEY, new IndividualSendJob.Factory()); - put(LeaveGroupV2Job.KEY, new LeaveGroupV2Job.Factory()); - put(LeaveGroupV2WorkerJob.KEY, new LeaveGroupV2WorkerJob.Factory()); - put(LinkedDeviceInactiveCheckJob.KEY, new LinkedDeviceInactiveCheckJob.Factory()); - put(LocalArchiveJob.KEY, new LocalArchiveJob.Factory()); - put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); - put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory()); - put(MarkerJob.KEY, new MarkerJob.Factory()); - put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory()); - put(MultiDeviceCallLinkSyncJob.KEY, new MultiDeviceCallLinkSyncJob.Factory()); - put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory()); - put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory()); - put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory()); - put(MultiDeviceDeleteSyncJob.KEY, new MultiDeviceDeleteSyncJob.Factory()); - put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory()); - put(MultiDeviceMessageRequestResponseJob.KEY, new MultiDeviceMessageRequestResponseJob.Factory()); - put(MultiDeviceOutgoingPaymentSyncJob.KEY, new MultiDeviceOutgoingPaymentSyncJob.Factory()); - put(MultiDeviceProfileContentUpdateJob.KEY, new MultiDeviceProfileContentUpdateJob.Factory()); - put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory()); - put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory()); - put(MultiDeviceStickerPackOperationJob.KEY, new MultiDeviceStickerPackOperationJob.Factory()); - put(MultiDeviceStickerPackSyncJob.KEY, new MultiDeviceStickerPackSyncJob.Factory()); - put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory()); - put(MultiDeviceSubscriptionSyncRequestJob.KEY, new MultiDeviceSubscriptionSyncRequestJob.Factory()); - put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory()); - put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory()); - put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory()); - put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); - put(OptimizeMediaJob.KEY, new OptimizeMediaJob.Factory()); - put(OptimizeMessageSearchIndexJob.KEY, new OptimizeMessageSearchIndexJob.Factory()); - put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory()); - put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory()); - put(PaymentNotificationSendJobV2.KEY, new PaymentNotificationSendJobV2.Factory()); - put(PaymentSendJob.KEY, new PaymentSendJob.Factory()); - put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); - put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); - put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); - put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); - put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); - put(PushDistributionListSendJob.KEY, new PushDistributionListSendJob.Factory()); - put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); - put(PushGroupSilentUpdateSendJob.KEY, new PushGroupSilentUpdateSendJob.Factory()); - put(MessageFetchJob.KEY, new MessageFetchJob.Factory()); - put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory()); - put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory()); - put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); - put(ReactionSendJob.KEY, new ReactionSendJob.Factory()); - put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory()); - put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory()); - put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); - put(RefreshCallLinkDetailsJob.KEY, new RefreshCallLinkDetailsJob.Factory()); - put(RefreshSvrCredentialsJob.KEY, new RefreshSvrCredentialsJob.Factory()); - put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory()); - put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory()); - put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory()); - put(ReportSpamJob.KEY, new ReportSpamJob.Factory()); - put(ResendMessageJob.KEY, new ResendMessageJob.Factory()); - put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory()); - put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory()); - put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); - put(RestoreAttachmentJob.KEY, new RestoreAttachmentJob.Factory()); - put(RestoreAttachmentThumbnailJob.KEY, new RestoreAttachmentThumbnailJob.Factory()); - put(RestoreLocalAttachmentJob.KEY, new RestoreLocalAttachmentJob.Factory()); - put(RestoreOptimizedMediaJob.KEY, new RestoreOptimizedMediaJob.Factory()); - put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory()); - put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory()); - put(RetrieveRemoteAnnouncementsJob.KEY, new RetrieveRemoteAnnouncementsJob.Factory()); - put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory()); - put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory()); - put(SenderKeyDistributionSendJob.KEY, new SenderKeyDistributionSendJob.Factory()); - put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); - put(SendPaymentsActivatedJob.KEY, new SendPaymentsActivatedJob.Factory()); - put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); - put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); - put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); - put(StorageRotateManifestJob.KEY, new StorageRotateManifestJob.Factory()); - put(SyncSystemContactLinksJob.KEY, new SyncSystemContactLinksJob.Factory()); - put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory()); - put(ResetSvrGuessCountJob.KEY, new ResetSvrGuessCountJob.Factory()); - put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); - put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory()); - put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory()); - put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); - put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory()); - put(StorageSyncJob.KEY, new StorageSyncJob.Factory()); - put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory()); - put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory()); - put(Svr2MirrorJob.KEY, new Svr2MirrorJob.Factory()); - put(Svr3MirrorJob.KEY, new Svr3MirrorJob.Factory()); - put(SyncArchivedMediaJob.KEY, new SyncArchivedMediaJob.Factory()); - put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory()); - put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); - put(TypingSendJob.KEY, new TypingSendJob.Factory()); - put(UploadAttachmentToArchiveJob.KEY, new UploadAttachmentToArchiveJob.Factory()); + return new HashMap<>() {{ + put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory()); + put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory()); + put(ApkUpdateJob.KEY, new ApkUpdateJob.Factory()); + put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory()); + put(ArchiveThumbnailUploadJob.KEY, new ArchiveThumbnailUploadJob.Factory()); + put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); + put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory()); + put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); + put(AttachmentHashBackfillJob.KEY, new AttachmentHashBackfillJob.Factory()); + put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); + put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory()); + put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); + put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); + put(BackfillDigestJob.KEY, new BackfillDigestJob.Factory()); + put(BackfillDigestsForDataFileJob.KEY, new BackfillDigestsForDataFileJob.Factory()); + put(BackupMessagesJob.KEY, new BackupMessagesJob.Factory()); + put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory()); + put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory()); + put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory()); + put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory()); + put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory()); + put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory()); + put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory()); + put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory()); + put(CheckRestoreMediaLeftJob.KEY, new CheckRestoreMediaLeftJob.Factory()); + put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory()); + put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); + put(ConversationShortcutRankingUpdateJob.KEY, new ConversationShortcutRankingUpdateJob.Factory()); + put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory()); + put(CopyAttachmentToArchiveJob.KEY, new CopyAttachmentToArchiveJob.Factory()); + put(CreateReleaseChannelJob.KEY, new CreateReleaseChannelJob.Factory()); + put(DeleteAbandonedAttachmentsJob.KEY, new DeleteAbandonedAttachmentsJob.Factory()); + put(NewLinkedDeviceNotificationJob.KEY, new NewLinkedDeviceNotificationJob.Factory()); + put(DeviceNameChangeJob.KEY, new DeviceNameChangeJob.Factory()); + put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory()); + put(DownloadLatestEmojiDataJob.KEY, new DownloadLatestEmojiDataJob.Factory()); + put(EmojiSearchIndexDownloadJob.KEY, new EmojiSearchIndexDownloadJob.Factory()); + put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory()); + put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory()); + put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory()); + put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory()); + put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory()); + put(GenerateAudioWaveFormJob.KEY, new GenerateAudioWaveFormJob.Factory()); + put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory()); + put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory()); + put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory()); + put(GroupRingCleanupJob.KEY, new GroupRingCleanupJob.Factory()); + put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory()); + put(InAppPaymentAuthCheckJob.KEY, new InAppPaymentAuthCheckJob.Factory()); + put(InAppPaymentGiftSendJob.KEY, new InAppPaymentGiftSendJob.Factory()); + put(InAppPaymentKeepAliveJob.KEY, new InAppPaymentKeepAliveJob.Factory()); + put(InAppPaymentPurchaseTokenJob.KEY, new InAppPaymentPurchaseTokenJob.Factory()); + put(InAppPaymentRecurringContextJob.KEY, new InAppPaymentRecurringContextJob.Factory()); + put(InAppPaymentOneTimeContextJob.KEY, new InAppPaymentOneTimeContextJob.Factory()); + put(InAppPaymentRedemptionJob.KEY, new InAppPaymentRedemptionJob.Factory()); + put(IndividualSendJob.KEY, new IndividualSendJob.Factory()); + put(LeaveGroupV2Job.KEY, new LeaveGroupV2Job.Factory()); + put(LeaveGroupV2WorkerJob.KEY, new LeaveGroupV2WorkerJob.Factory()); + put(LinkedDeviceInactiveCheckJob.KEY, new LinkedDeviceInactiveCheckJob.Factory()); + put(LocalArchiveJob.KEY, new LocalArchiveJob.Factory()); + put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); + put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory()); + put(MarkerJob.KEY, new MarkerJob.Factory()); + put(MultiDeviceAttachmentBackfillMissingJob.KEY, new MultiDeviceAttachmentBackfillMissingJob.Factory()); + put(MultiDeviceAttachmentBackfillUpdateJob.KEY, new MultiDeviceAttachmentBackfillUpdateJob.Factory()); + put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory()); + put(MultiDeviceCallLinkSyncJob.KEY, new MultiDeviceCallLinkSyncJob.Factory()); + put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory()); + put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory()); + put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory()); + put(MultiDeviceDeleteSyncJob.KEY, new MultiDeviceDeleteSyncJob.Factory()); + put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory()); + put(MultiDeviceMessageRequestResponseJob.KEY, new MultiDeviceMessageRequestResponseJob.Factory()); + put(MultiDeviceOutgoingPaymentSyncJob.KEY, new MultiDeviceOutgoingPaymentSyncJob.Factory()); + put(MultiDeviceProfileContentUpdateJob.KEY, new MultiDeviceProfileContentUpdateJob.Factory()); + put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory()); + put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory()); + put(MultiDeviceStickerPackOperationJob.KEY, new MultiDeviceStickerPackOperationJob.Factory()); + put(MultiDeviceStickerPackSyncJob.KEY, new MultiDeviceStickerPackSyncJob.Factory()); + put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory()); + put(MultiDeviceSubscriptionSyncRequestJob.KEY, new MultiDeviceSubscriptionSyncRequestJob.Factory()); + put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory()); + put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory()); + put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory()); + put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(OptimizeMediaJob.KEY, new OptimizeMediaJob.Factory()); + put(OptimizeMessageSearchIndexJob.KEY, new OptimizeMessageSearchIndexJob.Factory()); + put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory()); + put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory()); + put(PaymentNotificationSendJobV2.KEY, new PaymentNotificationSendJobV2.Factory()); + put(PaymentSendJob.KEY, new PaymentSendJob.Factory()); + put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); + put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); + put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); + put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); + put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); + put(PushDistributionListSendJob.KEY, new PushDistributionListSendJob.Factory()); + put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); + put(PushGroupSilentUpdateSendJob.KEY, new PushGroupSilentUpdateSendJob.Factory()); + put(MessageFetchJob.KEY, new MessageFetchJob.Factory()); + put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory()); + put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory()); + put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); + put(ReactionSendJob.KEY, new ReactionSendJob.Factory()); + put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory()); + put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory()); + put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); + put(RefreshCallLinkDetailsJob.KEY, new RefreshCallLinkDetailsJob.Factory()); + put(RefreshSvrCredentialsJob.KEY, new RefreshSvrCredentialsJob.Factory()); + put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory()); + put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory()); + put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory()); + put(ReportSpamJob.KEY, new ReportSpamJob.Factory()); + put(ResendMessageJob.KEY, new ResendMessageJob.Factory()); + put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory()); + put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory()); + put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); + put(RestoreAttachmentJob.KEY, new RestoreAttachmentJob.Factory()); + put(RestoreAttachmentThumbnailJob.KEY, new RestoreAttachmentThumbnailJob.Factory()); + put(RestoreLocalAttachmentJob.KEY, new RestoreLocalAttachmentJob.Factory()); + put(RestoreOptimizedMediaJob.KEY, new RestoreOptimizedMediaJob.Factory()); + put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory()); + put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory()); + put(RetrieveRemoteAnnouncementsJob.KEY, new RetrieveRemoteAnnouncementsJob.Factory()); + put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory()); + put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory()); + put(SenderKeyDistributionSendJob.KEY, new SenderKeyDistributionSendJob.Factory()); + put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); + put(SendPaymentsActivatedJob.KEY, new SendPaymentsActivatedJob.Factory()); + put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); + put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); + put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); + put(StorageRotateManifestJob.KEY, new StorageRotateManifestJob.Factory()); + put(SyncSystemContactLinksJob.KEY, new SyncSystemContactLinksJob.Factory()); + put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory()); + put(ResetSvrGuessCountJob.KEY, new ResetSvrGuessCountJob.Factory()); + put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); + put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory()); + put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory()); + put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); + put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory()); + put(StorageSyncJob.KEY, new StorageSyncJob.Factory()); + put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory()); + put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory()); + put(Svr2MirrorJob.KEY, new Svr2MirrorJob.Factory()); + put(Svr3MirrorJob.KEY, new Svr3MirrorJob.Factory()); + put(SyncArchivedMediaJob.KEY, new SyncArchivedMediaJob.Factory()); + put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory()); + put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); + put(TypingSendJob.KEY, new TypingSendJob.Factory()); + put(UploadAttachmentToArchiveJob.KEY, new UploadAttachmentToArchiveJob.Factory()); // Migrations put(AccountConsistencyMigrationJob.KEY, new AccountConsistencyMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillMissingJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillMissingJob.kt new file mode 100644 index 0000000000..b498346f14 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillMissingJob.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.protos.MultiDeviceAttachmentBackfillMissingJobData +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage +import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException +import org.whispersystems.signalservice.internal.push.AddressableMessage +import org.whispersystems.signalservice.internal.push.ConversationIdentifier +import org.whispersystems.signalservice.internal.push.SyncMessage +import java.io.IOException +import kotlin.time.Duration.Companion.days + +/** + * Tells linked devices that the requested message from a [SyncMessage.attachmentBackfillRequest] could not be found. + */ +class MultiDeviceAttachmentBackfillMissingJob( + parameters: Parameters, + private val targetMessage: AddressableMessage, + private val targetConversation: ConversationIdentifier +) : Job(parameters) { + + companion object { + private val TAG = Log.tag(MultiDeviceAttachmentBackfillMissingJob::class) + + const val KEY = "MultiDeviceAttachmentBackfillMissingJob" + + fun enqueue(targetMessage: AddressableMessage, targetConversation: ConversationIdentifier) { + AppDependencies.jobManager.add(MultiDeviceAttachmentBackfillMissingJob(targetMessage, targetConversation)) + } + } + + constructor(targetMessage: AddressableMessage, targetConversation: ConversationIdentifier) : this( + Parameters.Builder() + .setLifespan(1.days.inWholeMilliseconds) + .setMaxAttempts(Parameters.UNLIMITED) + .addConstraint(NetworkConstraint.KEY) + .build(), + targetMessage, + targetConversation + ) + + override fun getFactoryKey(): String = KEY + + override fun serialize(): ByteArray { + return MultiDeviceAttachmentBackfillMissingJobData( + targetMessage = targetMessage, + targetConversation = targetConversation + ).encode() + } + + override fun run(): Result { + val syncMessage = SignalServiceSyncMessage.forAttachmentBackfillResponse( + SyncMessage.AttachmentBackfillResponse( + targetMessage = targetMessage, + targetConversation = targetConversation, + error = SyncMessage.AttachmentBackfillResponse.Error.MESSAGE_NOT_FOUND + ) + ) + + return try { + val result = AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage) + if (result.isSuccess) { + Log.i(TAG, "[${targetMessage.sentTimestamp}] Successfully sent backfill missing message response.") + Result.success() + } else { + Log.w(TAG, "[${targetMessage.sentTimestamp}] Non-successful result. Retrying.") + Result.retry(defaultBackoff()) + } + } catch (e: ServerRejectedException) { + Log.w(TAG, e) + Result.failure() + } catch (e: IOException) { + Log.w(TAG, e) + Result.retry(defaultBackoff()) + } catch (e: UntrustedIdentityException) { + Log.w(TAG, e) + Result.failure() + } + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceAttachmentBackfillMissingJob { + val data = MultiDeviceAttachmentBackfillMissingJobData.ADAPTER.decode(serializedData!!) + return MultiDeviceAttachmentBackfillMissingJob( + parameters, + data.targetMessage!!, + data.targetConversation!! + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillUpdateJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillUpdateJob.kt new file mode 100644 index 0000000000..f4a54f91d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceAttachmentBackfillUpdateJob.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.attachments.toAttachmentPointer +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.protos.MultiDeviceAttachmentBackfillUpdateJobData +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage +import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException +import org.whispersystems.signalservice.internal.push.AddressableMessage +import org.whispersystems.signalservice.internal.push.ConversationIdentifier +import org.whispersystems.signalservice.internal.push.SyncMessage +import org.whispersystems.signalservice.internal.push.SyncMessage.AttachmentBackfillResponse.AttachmentData +import java.io.IOException +import kotlin.time.Duration.Companion.days + +/** + * Tells linked devices about all the attachments that have been re-uploaded for a given [SyncMessage.attachmentBackfillRequest]. + */ +class MultiDeviceAttachmentBackfillUpdateJob( + parameters: Parameters, + private val targetMessage: AddressableMessage, + private val targetConversation: ConversationIdentifier, + private val messageId: Long +) : Job(parameters) { + + companion object { + private val TAG = Log.tag(MultiDeviceAttachmentBackfillUpdateJob::class) + + const val KEY = "MultiDeviceAttachmentBackfillUpdateJob" + + private val JOB_LIFESPAN = 1.days.inWholeMilliseconds + private val UPLOAD_THRESHOLD = AttachmentUploadJob.UPLOAD_REUSE_THRESHOLD + JOB_LIFESPAN + + fun enqueue(targetMessage: AddressableMessage, targetConversation: ConversationIdentifier, messageId: Long) { + AppDependencies.jobManager.add(MultiDeviceAttachmentBackfillUpdateJob(targetMessage, targetConversation, messageId)) + } + } + + constructor( + targetMessage: AddressableMessage, + targetConversation: ConversationIdentifier, + messageId: Long + ) : this( + Parameters.Builder() + .setLifespan(JOB_LIFESPAN) + .setMaxAttempts(Parameters.UNLIMITED) + .addConstraint(NetworkConstraint.KEY) + .build(), + targetMessage, + targetConversation, + messageId + ) + + override fun getFactoryKey(): String = KEY + + override fun serialize(): ByteArray { + return MultiDeviceAttachmentBackfillUpdateJobData( + targetMessage = targetMessage, + targetConversation = targetConversation, + messageId = messageId + ).encode() + } + + override fun run(): Result { + val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId).sortedBy { it.displayOrder } + if (attachments.isEmpty()) { + Log.w(TAG, "Failed to find any attachments for the message! Sending a missing response.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(targetMessage, targetConversation) + return Result.failure() + } + + val attachmentDatas = attachments.map { attachment -> + when { + attachment.hasData && !attachment.isInProgress && attachment.withinUploadThreshold() -> { + AttachmentData(attachment = attachment.toAttachmentPointer(context)) + } + !attachment.hasData || attachment.isPermanentlyFailed -> { + AttachmentData(status = AttachmentData.Status.TERMINAL_ERROR) + } + else -> { + AttachmentData(status = AttachmentData.Status.PENDING) + } + } + } + + val syncMessage = SignalServiceSyncMessage.forAttachmentBackfillResponse( + SyncMessage.AttachmentBackfillResponse( + targetMessage = targetMessage, + targetConversation = targetConversation, + attachments = SyncMessage.AttachmentBackfillResponse.AttachmentDataList( + attachments = attachmentDatas + ) + ) + ) + + return try { + val result = AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage) + if (result.isSuccess) { + Log.i(TAG, "[${targetMessage.sentTimestamp}] Successfully sent backfill update message response.") + Result.success() + } else { + Log.w(TAG, "[${targetMessage.sentTimestamp}] Non-successful result. Retrying.") + Result.retry(defaultBackoff()) + } + } catch (e: ServerRejectedException) { + Log.w(TAG, e) + Result.failure() + } catch (e: IOException) { + Log.w(TAG, e) + Result.retry(defaultBackoff()) + } catch (e: UntrustedIdentityException) { + Log.w(TAG, e) + Result.failure() + } + } + + override fun onFailure() { + if (isCascadingFailure) { + Log.w(TAG, "The upload job failed! Enqueuing another instance of the job to notify of the failure.") + MultiDeviceAttachmentBackfillUpdateJob.enqueue(targetMessage, targetConversation, messageId) + } + } + + private fun DatabaseAttachment.withinUploadThreshold(): Boolean { + return this.uploadTimestamp > 0 && System.currentTimeMillis() - this.uploadTimestamp < UPLOAD_THRESHOLD + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceAttachmentBackfillUpdateJob { + val data = MultiDeviceAttachmentBackfillUpdateJobData.ADAPTER.decode(serializedData!!) + return MultiDeviceAttachmentBackfillUpdateJob( + parameters, + data.targetMessage!!, + data.targetConversation!!, + data.messageId + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSyncJob.kt index 0b4c8b35e4..4d27a051d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSyncJob.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData -import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AddressableMessage import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AttachmentDelete import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.ThreadDelete import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -28,7 +27,9 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException import org.whispersystems.signalservice.api.util.UuidUtil +import org.whispersystems.signalservice.internal.push.AddressableMessage import org.whispersystems.signalservice.internal.push.Content +import org.whispersystems.signalservice.internal.push.ConversationIdentifier import org.whispersystems.signalservice.internal.push.SyncMessage import org.whispersystems.signalservice.internal.push.SyncMessage.DeleteForMe import java.io.IOException @@ -108,7 +109,7 @@ class MultiDeviceDeleteSyncJob private constructor( } @WorkerThread - private fun createMessageDeletes(messageRecords: Collection): List { + private fun createMessageDeletes(messageRecords: Collection): List { return messageRecords.mapNotNull { message -> val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId) if (threadRecipient == null) { @@ -120,7 +121,7 @@ class MultiDeviceDeleteSyncJob private constructor( } else if (threadRecipient.isDistributionList || !message.canDeleteSync()) { null } else { - AddressableMessage( + DeleteSyncJobData.AddressableMessage( threadRecipientId = threadRecipient.id.toLong(), sentTimestamp = message.dateSent, authorRecipientId = message.fromRecipient.id.toLong() @@ -145,7 +146,7 @@ class MultiDeviceDeleteSyncJob private constructor( } else if (threadRecipient.isDistributionList || !message.canDeleteSync()) { null } else { - AddressableMessage( + DeleteSyncJobData.AddressableMessage( threadRecipientId = threadRecipient.id.toLong(), sentTimestamp = message.dateSent, authorRecipientId = message.fromRecipient.id.toLong() @@ -188,13 +189,13 @@ class MultiDeviceDeleteSyncJob private constructor( threadRecipientId = threadRecipient.id.toLong(), isFullDelete = isFullDelete, messages = messages.map { - AddressableMessage( + DeleteSyncJobData.AddressableMessage( sentTimestamp = it.dateSent, authorRecipientId = it.fromRecipient.id.toLong() ) }, nonExpiringMessages = nonExpiringMessages.map { - AddressableMessage( + DeleteSyncJobData.AddressableMessage( sentTimestamp = it.dateSent, authorRecipientId = it.fromRecipient.id.toLong() ) @@ -207,7 +208,7 @@ class MultiDeviceDeleteSyncJob private constructor( @VisibleForTesting constructor( - messages: List = emptyList(), + messages: List = emptyList(), threads: List = emptyList(), localOnlyThreads: List = emptyList(), attachments: List = emptyList() @@ -370,17 +371,17 @@ class MultiDeviceDeleteSyncJob private constructor( return Content(syncMessage = syncMessage.build()) } - private fun Recipient.toDeleteSyncConversationId(): DeleteForMe.ConversationIdentifier? { + private fun Recipient.toDeleteSyncConversationId(): ConversationIdentifier? { return when { - isGroup -> DeleteForMe.ConversationIdentifier(threadGroupId = requireGroupId().decodedId.toByteString()) - hasAci -> DeleteForMe.ConversationIdentifier(threadServiceId = requireAci().toString()) - hasPni -> DeleteForMe.ConversationIdentifier(threadServiceId = requirePni().toString()) - hasE164 -> DeleteForMe.ConversationIdentifier(threadE164 = requireE164()) + isGroup -> ConversationIdentifier(threadGroupId = requireGroupId().decodedId.toByteString()) + hasAci -> ConversationIdentifier(threadServiceId = requireAci().toString()) + hasPni -> ConversationIdentifier(threadServiceId = requirePni().toString()) + hasE164 -> ConversationIdentifier(threadE164 = requireE164()) else -> null } } - private fun AddressableMessage.toDeleteSyncMessage(): DeleteForMe.AddressableMessage? { + private fun DeleteSyncJobData.AddressableMessage.toDeleteSyncMessage(): AddressableMessage? { val author: Recipient = Recipient.resolved(RecipientId.from(authorRecipientId)) val authorServiceId: String? = author.aci.orNull()?.toString() ?: author.pni.orNull()?.toString() val authorE164: String? = if (authorServiceId == null) { @@ -393,7 +394,7 @@ class MultiDeviceDeleteSyncJob private constructor( Log.w(TAG, "Unable to send sync message without serviceId or e164 recipient: ${author.id}") null } else { - DeleteForMe.AddressableMessage( + AddressableMessage( authorServiceId = authorServiceId, authorE164 = authorE164, sentTimestamp = sentTimestamp diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 8c507470ac..df23d4e6c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -47,6 +47,9 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.GroupChangeBusyException import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob +import org.thoughtcrime.securesms.jobs.AttachmentUploadJob +import org.thoughtcrime.securesms.jobs.MultiDeviceAttachmentBackfillMissingJob +import org.thoughtcrime.securesms.jobs.MultiDeviceAttachmentBackfillUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob @@ -95,6 +98,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry import org.thoughtcrime.securesms.util.IdentityUtil import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MessageConstraintsUtil +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata @@ -106,7 +110,9 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.storage.StorageKey import org.whispersystems.signalservice.api.util.UuidUtil +import org.whispersystems.signalservice.internal.push.AddressableMessage import org.whispersystems.signalservice.internal.push.Content +import org.whispersystems.signalservice.internal.push.ConversationIdentifier import org.whispersystems.signalservice.internal.push.DataMessage import org.whispersystems.signalservice.internal.push.EditMessage import org.whispersystems.signalservice.internal.push.Envelope @@ -129,6 +135,7 @@ import java.util.Optional import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.time.Duration +import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds object SyncMessageProcessor { @@ -162,6 +169,8 @@ object SyncMessageProcessor { syncMessage.callLinkUpdate != null -> handleSynchronizeCallLink(syncMessage.callLinkUpdate!!, envelope.timestamp!!) syncMessage.callLogEvent != null -> handleSynchronizeCallLogEvent(syncMessage.callLogEvent!!, envelope.timestamp!!) syncMessage.deleteForMe != null -> handleSynchronizeDeleteForMe(context, syncMessage.deleteForMe!!, envelope.timestamp!!, earlyMessageCacheEntry) + syncMessage.attachmentBackfillRequest != null -> handleSynchronizeAttachmentBackfillRequest(syncMessage.attachmentBackfillRequest!!, envelope.timestamp!!) + syncMessage.attachmentBackfillResponse != null -> warn(envelope.timestamp!!, "Contains a backfill response, but we don't handle these!") else -> warn(envelope.timestamp!!, "Contains no known sync types...") } } @@ -1634,7 +1643,70 @@ object SyncMessageProcessor { } } - private fun SyncMessage.DeleteForMe.ConversationIdentifier.toRecipientId(): RecipientId? { + private fun handleSynchronizeAttachmentBackfillRequest(request: SyncMessage.AttachmentBackfillRequest, timestamp: Long) { + if (!RemoteConfig.attachmentBackfillSync) { + warn(timestamp, "[AttachmentBackfillRequest] Remote config not enabled! Skipping.") + return + } + + if (request.targetMessage == null || request.targetConversation == null) { + warn(timestamp, "[AttachmentBackfillRequest] Target message or target conversation was unset! Can't formulate a response, ignoring.") + return + } + + val syncMessageId = request.targetMessage!!.toSyncMessageId(timestamp) + if (syncMessageId == null) { + warn(timestamp, "[AttachmentBackfillRequest] Invalid targetMessageId! Can't formulate a response, ignoring.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(request.targetMessage!!, request.targetConversation!!) + return + } + + val conversationRecipientId: RecipientId? = request.targetConversation!!.toRecipientId() + if (conversationRecipientId == null) { + warn(timestamp, "[AttachmentBackfillRequest] Failed to find the target conversation! Enqueuing a 'missing' response.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(request.targetMessage!!, request.targetConversation!!) + return + } + + val threadId = SignalDatabase.threads.getThreadIdFor(conversationRecipientId) + if (threadId == null) { + warn(timestamp, "[AttachmentBackfillRequest] No thread exists for the conversation! Enqueuing a 'missing' response.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(request.targetMessage!!, request.targetConversation!!) + return + } + + val messageId: Long? = SignalDatabase.messages.getMessageIdOrNull(syncMessageId, threadId) + if (messageId == null) { + warn(timestamp, "[AttachmentBackfillRequest] Unable to find message! Enqueuing a 'missing' response.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(request.targetMessage!!, request.targetConversation!!) + return + } + + val attachments: List = SignalDatabase.attachments.getAttachmentsForMessage(messageId).sortedBy { it.displayOrder } + if (attachments.isEmpty()) { + warn(timestamp, "[AttachmentBackfillRequest] There were no attachments found for the message! Enqueuing a 'missing' response.") + MultiDeviceAttachmentBackfillMissingJob.enqueue(request.targetMessage!!, request.targetConversation!!) + return + } + + val now = System.currentTimeMillis() + val needsUpload = attachments.filter { now - it.uploadTimestamp > 3.days.inWholeMilliseconds } + log(timestamp, "[AttachmentBackfillRequest] ${needsUpload.size}/${attachments.size} attachments need to be re-uploaded.") + + for (attachment in needsUpload) { + AppDependencies.jobManager + .startChain(AttachmentUploadJob(attachment.attachmentId)) + .then(MultiDeviceAttachmentBackfillUpdateJob(request.targetMessage!!, request.targetConversation!!, messageId)) + .enqueue() + } + + if (needsUpload.size != attachments.size) { + log(timestamp, "[AttachmentBackfillRequest] At least one attachment didn't need to be uploaded. Enqueuing update job immediately.") + MultiDeviceAttachmentBackfillUpdateJob.enqueue(request.targetMessage!!, request.targetConversation!!, messageId) + } + } + + private fun ConversationIdentifier.toRecipientId(): RecipientId? { return when { threadGroupId != null -> { try { @@ -1659,7 +1731,7 @@ object SyncMessageProcessor { } } - private fun SyncMessage.DeleteForMe.AddressableMessage.toSyncMessageId(envelopeTimestamp: Long): MessageTable.SyncMessageId? { + private fun AddressableMessage.toSyncMessageId(envelopeTimestamp: Long): MessageTable.SyncMessageId? { return if (this.sentTimestamp != null && (this.authorServiceId != null || this.authorE164 != null)) { val serviceId = ServiceId.parseOrNull(this.authorServiceId) val id = if (serviceId != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 700e3b0848..c452792c0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1127,5 +1127,12 @@ object RemoteConfig { hotSwappable = true ) + /** Whether or not this device respect attachment backfill requests. */ + val attachmentBackfillSync: Boolean by remoteBoolean( + key = "android.attachmentBackfillSync", + defaultValue = false, + hotSwappable = true + ) + // endregion } diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index c8b40c4412..d17805dc00 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -7,6 +7,7 @@ import "ResumableUploads.proto"; option java_package = "org.thoughtcrime.securesms.jobs.protos"; option java_multiple_files = true; +import SignalService.proto; message CallSyncEventJobRecord { @@ -157,4 +158,15 @@ message BackupMessagesJobData { message NewLinkedDeviceNotificationJobData { uint32 deviceId = 1; uint64 deviceCreatedAt = 2; -} \ No newline at end of file +} + +message MultiDeviceAttachmentBackfillMissingJobData { + signalservice.AddressableMessage targetMessage = 1; + signalservice.ConversationIdentifier targetConversation = 2; +} + +message MultiDeviceAttachmentBackfillUpdateJobData { + signalservice.AddressableMessage targetMessage = 1; + signalservice.ConversationIdentifier targetConversation = 2; + uint64 messageId = 3; +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index dcf75e17e1..7ad3f42ec6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -761,6 +761,8 @@ public class SignalServiceMessageSender { content = createCallLogEventContent(message.getCallLogEvent().get()); } else if (message.getDeviceNameChange().isPresent()) { content = createDeviceNameChangeContent(message.getDeviceNameChange().get()); + } else if (message.getAttachmentBackfillResponse().isPresent()) { + content = createAttachmentBackfillResponseContent(message.getAttachmentBackfillResponse().get()); } else { throw new IOException("Unsupported sync message!"); } @@ -1740,6 +1742,13 @@ public class SignalServiceMessageSender { return container.syncMessage(builder.build()).build(); } + private Content createAttachmentBackfillResponseContent(SyncMessage.AttachmentBackfillResponse proto) { + Content.Builder container = new Content.Builder(); + SyncMessage.Builder builder = createSyncMessageBuilder().attachmentBackfillResponse(proto); + + return container.syncMessage(builder.build()).build(); + } + private SyncMessage.Builder createSyncMessageBuilder() { byte[] padding = Util.getRandomLengthSecretBytes(512); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java index f54b3a13bc..53c4251367 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java @@ -6,6 +6,8 @@ package org.whispersystems.signalservice.api.messages.multidevice; +import org.whispersystems.signalservice.internal.push.SyncMessage; +import org.whispersystems.signalservice.internal.push.SyncMessage.AttachmentBackfillResponse; import org.whispersystems.signalservice.internal.push.SyncMessage.DeviceNameChange; import org.whispersystems.signalservice.internal.push.SyncMessage.CallEvent; import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate; @@ -37,6 +39,7 @@ public class SignalServiceSyncMessage { private final Optional callLinkUpdate; private final Optional callLogEvent; private final Optional deviceNameChange; + private final Optional attachmentBackfillResponse; private SignalServiceSyncMessage(Optional sent, Optional contacts, @@ -55,26 +58,28 @@ public class SignalServiceSyncMessage { Optional callEvent, Optional callLinkUpdate, Optional callLogEvent, - Optional deviceNameChange) + Optional deviceNameChange, + Optional attachmentBackfillResponse) { - this.sent = sent; - this.contacts = contacts; - this.blockedList = blockedList; - this.request = request; - this.reads = reads; - this.viewOnceOpen = viewOnceOpen; - this.verified = verified; - this.configuration = configuration; - this.stickerPackOperations = stickerPackOperations; - this.fetchType = fetchType; - this.keys = keys; - this.messageRequestResponse = messageRequestResponse; - this.outgoingPaymentMessage = outgoingPaymentMessage; - this.views = views; - this.callEvent = callEvent; - this.callLinkUpdate = callLinkUpdate; - this.callLogEvent = callLogEvent; - this.deviceNameChange = deviceNameChange; + this.sent = sent; + this.contacts = contacts; + this.blockedList = blockedList; + this.request = request; + this.reads = reads; + this.viewOnceOpen = viewOnceOpen; + this.verified = verified; + this.configuration = configuration; + this.stickerPackOperations = stickerPackOperations; + this.fetchType = fetchType; + this.keys = keys; + this.messageRequestResponse = messageRequestResponse; + this.outgoingPaymentMessage = outgoingPaymentMessage; + this.views = views; + this.callEvent = callEvent; + this.callLinkUpdate = callLinkUpdate; + this.callLogEvent = callLogEvent; + this.deviceNameChange = deviceNameChange; + this.attachmentBackfillResponse = attachmentBackfillResponse; } public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) { @@ -95,6 +100,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -116,6 +122,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -137,6 +144,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -158,6 +166,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -179,6 +188,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -200,6 +210,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -224,6 +235,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -245,6 +257,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -266,6 +279,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -287,6 +301,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -308,6 +323,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -329,6 +345,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -350,6 +367,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -371,6 +389,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -392,6 +411,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -413,6 +433,7 @@ public class SignalServiceSyncMessage { Optional.of(callEvent), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -434,6 +455,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.of(callLinkUpdate), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -455,6 +477,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.of(callLogEvent), + Optional.empty(), Optional.empty()); } @@ -476,7 +499,30 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), - Optional.of(deviceNameChange)); + Optional.of(deviceNameChange), + Optional.empty()); + } + + public static SignalServiceSyncMessage forAttachmentBackfillResponse(@Nonnull AttachmentBackfillResponse backfillResponse) { + return new SignalServiceSyncMessage(Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of(backfillResponse)); } public static SignalServiceSyncMessage empty() { @@ -497,6 +543,7 @@ public class SignalServiceSyncMessage { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -572,6 +619,10 @@ public class SignalServiceSyncMessage { return deviceNameChange; } + public Optional getAttachmentBackfillResponse() { + return attachmentBackfillResponse; + } + public enum FetchType { LOCAL_PROFILE, STORAGE_MANIFEST, diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index 843af2d902..7b27a0ba59 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -650,22 +650,6 @@ message SyncMessage { } message DeleteForMe { - message ConversationIdentifier { - oneof identifier { - string threadServiceId = 1; - bytes threadGroupId = 2; - string threadE164 = 3; - } - } - - message AddressableMessage { - oneof author { - string authorServiceId = 1; - string authorE164 = 2; - } - optional uint64 sentTimestamp = 3; - } - message MessageDeletes { optional ConversationIdentifier conversation = 1; repeated AddressableMessage messages = 2; @@ -704,6 +688,41 @@ message SyncMessage { optional uint32 deviceId = 2; } + message AttachmentBackfillRequest { + optional AddressableMessage targetMessage = 1; + optional ConversationIdentifier targetConversation = 2; + } + + message AttachmentBackfillResponse { + message AttachmentData { + enum Status { + PENDING = 0; + TERMINAL_ERROR = 1; + } + + oneof data { + AttachmentPointer attachment = 1; + Status status = 2; + } + } + + enum Error { + MESSAGE_NOT_FOUND = 0; + } + + message AttachmentDataList { + repeated AttachmentData attachments = 1; + } + + optional AddressableMessage targetMessage = 1; + optional ConversationIdentifier targetConversation = 2; + + oneof data { + AttachmentDataList attachments = 3; + Error error = 4; + } + } + optional Sent sent = 1; optional Contacts contacts = 2; reserved /*groups*/ 3; @@ -727,6 +746,8 @@ message SyncMessage { optional CallLogEvent callLogEvent = 21; optional DeleteForMe deleteForMe = 22; optional DeviceNameChange deviceNameChange = 23; + optional AttachmentBackfillRequest attachmentBackfillRequest = 24; + optional AttachmentBackfillResponse attachmentBackfillResponse = 25; } message AttachmentPointer { @@ -836,4 +857,20 @@ message BodyRange { string mentionAci = 3; Style style = 4; } +} + +message AddressableMessage { + oneof author { + string authorServiceId = 1; + string authorE164 = 2; + } + optional uint64 sentTimestamp = 3; +} + +message ConversationIdentifier { + oneof identifier { + string threadServiceId = 1; + bytes threadGroupId = 2; + string threadE164 = 3; + } } \ No newline at end of file