From a3f900324fd35a51e40b4aefdc238f4a66f825bc Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 17 Sep 2026 13:41:20 +0000 Subject: [PATCH] Respect server retry-after for challenged sends in IndividualSendJobV2. --- .../securesms/jobs/IndividualSendJobV2.kt | 21 +++++++++++++++++-- .../securesms/ratelimit/RateLimitUtil.java | 3 +++ .../securesms/jobs/IndividualSendJobV2Test.kt | 6 ++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt index 87b43b4f31..91f617ea80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt @@ -114,6 +114,11 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va ) } + @JvmStatic + fun getMessageId(serializedData: ByteArray?): Long { + return IndividualSendJobV2Data.ADAPTER.decode(serializedData!!).messageId + } + private fun logPrefix(sentTimestamp: Long? = null, messageId: Long): String = "[${sentTimestamp ?: "?"}][$messageId]" } @@ -256,6 +261,12 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va } ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(recipient) + + if (SignalStore.rateLimit.needsRecaptcha()) { + Log.i(TAG, "${logPrefix(message.sentTimeMillis)} Successfully sent message. Assuming reCAPTCHA no longer needed.") + SignalStore.rateLimit.onProofAccepted() + } + Log.i(TAG, "${logPrefix(message.sentTimeMillis)} Sent message.") Result.success() }, @@ -302,8 +313,14 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId) when (ProofRequiredExceptionHandler.handle(context, proofException, threadRecipient, threadId, messageId)) { ProofRequiredExceptionHandler.Result.RETRY_NOW -> Result.retry(0L) - ProofRequiredExceptionHandler.Result.RETRY_LATER, - ProofRequiredExceptionHandler.Result.RETHROW -> Result.retry(nextRunAttemptBackoff(runAttempt + 1)) + ProofRequiredExceptionHandler.Result.RETRY_LATER -> Result.retry(nextRunAttemptBackoff(runAttempt + 1)) + ProofRequiredExceptionHandler.Result.RETHROW -> { + val defaultBackoff = nextRunAttemptBackoff(runAttempt + 1) + val serverBackoff = error.retryAfter?.inWholeMilliseconds ?: 0L + val backoff = maxOf(defaultBackoff, serverBackoff) + Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unresolved challenge, retryAfter=${error.retryAfter}, using backoff=${backoff}ms") + Result.retry(backoff) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RateLimitUtil.java b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RateLimitUtil.java index 4e4bc9992a..d2b5f5794f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RateLimitUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RateLimitUtil.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.IndividualSendJob; +import org.thoughtcrime.securesms.jobs.IndividualSendJobV2; import java.util.Set; @@ -37,6 +38,8 @@ public final class RateLimitUtil { AppDependencies.getJobManager().update((job) -> { if (job.getFactoryKey().equals(IndividualSendJob.KEY) && messageIds.contains(IndividualSendJob.getMessageId(job.getSerializedData()))) { return job.withNextBackoffInterval(0); + } else if (job.getFactoryKey().equals(IndividualSendJobV2.KEY) && messageIds.contains(IndividualSendJobV2.getMessageId(job.getSerializedData()))) { + return job.withNextBackoffInterval(0); } else if (job.getFactoryKey().equals(PushGroupSendJob.KEY) && messageIds.contains(PushGroupSendJob.getMessageId(job.getSerializedData()))) { return job.withNextBackoffInterval(0); } else { diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt b/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt index 8f2916d3e1..fdae6b74ec 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt @@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.keyvalue.MiscellaneousValues +import org.thoughtcrime.securesms.keyvalue.RateLimitValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.OutgoingMessage import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler @@ -106,6 +107,7 @@ class IndividualSendJobV2Test { private val recipientId: RecipientId = RecipientId.from(2L) private lateinit var misc: MiscellaneousValues + private lateinit var rateLimit: RateLimitValues private lateinit var recipient: Recipient private lateinit var self: Recipient @@ -129,6 +131,10 @@ class IndividualSendJobV2Test { every { misc.isClientDeprecated } returns false every { SignalStore.misc } returns misc + rateLimit = mockk(relaxUnitFun = true) + every { rateLimit.needsRecaptcha() } returns false + every { SignalStore.rateLimit } returns rateLimit + every { signalStore.account.aci } returns selfAci every { signalStore.account.requireAci() } returns selfAci every { signalStore.account.isMultiDevice } returns false