Add support for AttachmentBackfill sync messages.

This commit is contained in:
Greyson Parrelli
2025-02-13 11:46:09 -05:00
parent e1511a09a7
commit 754d759d7d
18 changed files with 781 additions and 229 deletions

View File

@@ -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
),

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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<SyncMessageId>): List<SyncMessageId> {
val threads = mutableSetOf<Long>()
val unhandled = mutableListOf<SyncMessageId>()

View File

@@ -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());

View File

@@ -145,7 +145,10 @@ class JobController {
List<Job> dependents = onFailure(job);
job.setContext(application);
job.onFailure();
Stream.of(dependents).forEach(Job::onFailure);
for (Job child : dependents) {
child.markCascadingFailure();
child.onFailure();
}
return;
}

View File

@@ -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<? extends Job> jobs) {
if (!jobs.isEmpty()) {
this.jobs.add(0, new ArrayList<>(jobs));
}
return this;
}
public void enqueue() {
jobManager.enqueueChain(this);
}

View File

@@ -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

View File

@@ -113,156 +113,158 @@ import java.util.Map;
public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
return new HashMap<String, Job.Factory>() {{
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());

View File

@@ -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<MultiDeviceAttachmentBackfillMissingJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceAttachmentBackfillMissingJob {
val data = MultiDeviceAttachmentBackfillMissingJobData.ADAPTER.decode(serializedData!!)
return MultiDeviceAttachmentBackfillMissingJob(
parameters,
data.targetMessage!!,
data.targetConversation!!
)
}
}
}

View File

@@ -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<MultiDeviceAttachmentBackfillUpdateJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceAttachmentBackfillUpdateJob {
val data = MultiDeviceAttachmentBackfillUpdateJobData.ADAPTER.decode(serializedData!!)
return MultiDeviceAttachmentBackfillUpdateJob(
parameters,
data.targetMessage!!,
data.targetConversation!!,
data.messageId
)
}
}
}

View File

@@ -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<MessageRecord>): List<AddressableMessage> {
private fun createMessageDeletes(messageRecords: Collection<MessageRecord>): List<DeleteSyncJobData.AddressableMessage> {
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<AddressableMessage> = emptyList(),
messages: List<DeleteSyncJobData.AddressableMessage> = emptyList(),
threads: List<ThreadDelete> = emptyList(),
localOnlyThreads: List<ThreadDelete> = emptyList(),
attachments: List<AttachmentDelete> = 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

View File

@@ -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<DatabaseAttachment> = 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) {

View File

@@ -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
}

View File

@@ -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;
}
}
message MultiDeviceAttachmentBackfillMissingJobData {
signalservice.AddressableMessage targetMessage = 1;
signalservice.ConversationIdentifier targetConversation = 2;
}
message MultiDeviceAttachmentBackfillUpdateJobData {
signalservice.AddressableMessage targetMessage = 1;
signalservice.ConversationIdentifier targetConversation = 2;
uint64 messageId = 3;
}

View File

@@ -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);

View File

@@ -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> callLinkUpdate;
private final Optional<CallLogEvent> callLogEvent;
private final Optional<DeviceNameChange> deviceNameChange;
private final Optional<AttachmentBackfillResponse> attachmentBackfillResponse;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
@@ -55,26 +58,28 @@ public class SignalServiceSyncMessage {
Optional<CallEvent> callEvent,
Optional<CallLinkUpdate> callLinkUpdate,
Optional<CallLogEvent> callLogEvent,
Optional<DeviceNameChange> deviceNameChange)
Optional<DeviceNameChange> deviceNameChange,
Optional<AttachmentBackfillResponse> 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<AttachmentBackfillResponse> getAttachmentBackfillResponse() {
return attachmentBackfillResponse;
}
public enum FetchType {
LOCAL_PROFILE,
STORAGE_MANIFEST,

View File

@@ -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;
}
}