From f7d87f3436654b6b50560fe2a92aec914acb312d Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Mon, 5 Jan 2026 12:56:13 -0500 Subject: [PATCH] Allow 1:1 polls and raise character limit. --- .../DataMessageProcessorTest_polls.kt | 7 --- .../securesms/backup/v2/ArchiveErrorCases.kt | 4 -- .../v2/exporters/ChatItemArchiveExporter.kt | 8 +-- .../conversation/v2/ConversationRepository.kt | 58 +++++++++++-------- .../conversation/v2/CreatePollFragment.kt | 9 ++- .../v2/keyboard/AttachmentKeyboardFragment.kt | 2 +- .../securesms/jobs/IndividualSendJob.java | 4 ++ .../securesms/jobs/PollVoteJob.kt | 20 +++++-- .../securesms/jobs/PushGroupSendJob.java | 26 ++------- .../securesms/jobs/PushSendJob.java | 18 ++++++ .../messages/DataMessageProcessor.kt | 44 +++++--------- .../messages/SyncMessageProcessor.kt | 29 +++------- .../securesms/sms/MessageSender.java | 7 +-- .../securesms/util/RemoteConfig.kt | 27 ++++----- app/src/main/res/values/strings.xml | 2 +- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 20 +++---- .../api/messages/EnvelopeContentValidator.kt | 3 +- .../messages/EnvelopeContentValidatorTest.kt | 4 +- 19 files changed, 138 insertions(+), 156 deletions(-) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt index 13d8adb0a3..e7045940f2 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt @@ -4,8 +4,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import assertk.assertThat import assertk.assertions.isEqualTo -import io.mockk.every -import io.mockk.mockkStatic import org.junit.Before import org.junit.Rule import org.junit.Test @@ -23,7 +21,6 @@ import org.thoughtcrime.securesms.testing.GroupTestingUtils import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember import org.thoughtcrime.securesms.testing.MessageContentFuzzer import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.util.RemoteConfig import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.internal.push.DataMessage @@ -42,10 +39,6 @@ class DataMessageProcessorTest_polls { @Before fun setUp() { - mockkStatic(RemoteConfig::class) - - every { RemoteConfig.receivePolls } returns true - alice = Recipient.resolved(harness.others[0]) bob = Recipient.resolved(harness.others[1]) charlie = Recipient.resolved(harness.others[2]) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt index 6b16d9cd34..b1140ed678 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt @@ -135,10 +135,6 @@ object ExportSkips { return log(sentTimestamp, "Poll option was invalid.") } - fun pollNotInGroupChat(sentTimestamp: Long): String { - return log(sentTimestamp, "Poll was not in a group chat.") - } - fun pinMessageIsInvalid(sentTimestamp: Long): String { return log(sentTimestamp, "Pin message update was invalid.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt index 94306802d3..3b8aaf5fe0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt @@ -122,6 +122,7 @@ private val TAG = Log.tag(ChatItemArchiveExporter::class.java) private val MAX_INLINED_BODY_SIZE = 128.kibiBytes.bytes.toInt() private val MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER = 2.kibiBytes.bytes.toInt() private val MAX_INLINED_QUOTE_BODY_SIZE = 2.kibiBytes.bytes.toInt() +private const val MAX_POLL_QUESTION_CHARACTER_LENGTH = 200 private const val MAX_POLL_CHARACTER_LENGTH = 100 private const val MAX_POLL_OPTIONS = 10 @@ -398,13 +399,8 @@ class ChatItemArchiveExporter( } extraData.pollsById[record.id] != null -> { - if (exportState.threadIdToRecipientId[builder.chatId] !in exportState.groupRecipientIds) { - Log.w(TAG, ExportSkips.pollNotInGroupChat(record.dateSent)) - continue - } - val poll = extraData.pollsById[record.id]!! - if (poll.question.isEmpty() || poll.question.length > MAX_POLL_CHARACTER_LENGTH) { + if (poll.question.isEmpty() || poll.question.length > MAX_POLL_QUESTION_CHARACTER_LENGTH) { Log.w(TAG, ExportSkips.invalidPollQuestion(record.dateSent)) continue } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index f1df4c417f..d16db1d248 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -218,12 +218,11 @@ class ConversationRepository( val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)!! val pollSentTimestamp = messageRecord.dateSent - if (threadRecipient.groupId.getOrNull()?.isV2 != true) { + if (threadRecipient.isPushV2Group && threadRecipient.groupId.getOrNull()?.isV2 != true) { Log.w(TAG, "Missing group id") emitter.tryOnError(Exception("Poll terminate failed")) } - val groupId = threadRecipient.requireGroupId().requireV2() val message = OutgoingMessage.pollTerminateMessage( threadRecipient = threadRecipient, sentTimeMillis = System.currentTimeMillis(), @@ -233,12 +232,17 @@ class ConversationRepository( Log.i(TAG, "Sending poll terminate to " + message.threadRecipient.id + ", thread: " + messageRecord.threadId) - val possibleTargets: List = SignalDatabase.groups.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF) - .map { it.resolve() } - .distinctBy { it.id } + val possibleTargets: List = if (threadRecipient.isPushV2Group) { + SignalDatabase.groups.getGroupMembers(threadRecipient.requireGroupId().requireV2(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF) + .map { it.resolve() } + .distinctBy { it.id } + } else { + listOf(threadRecipient) + } + val isSelf = threadRecipient.isSelf val eligibleTargets: List = RecipientUtil.getEligibleForSending(possibleTargets) - val results = sendEndPoll(threadRecipient, message, eligibleTargets, poll.messageId) + val results = sendEndPoll(threadRecipient, message, eligibleTargets, isSelf, poll.messageId) val sendResults = GroupSendJobHelper.getCompletedSends(eligibleTargets, results) if (sendResults.completed.isNotEmpty() || possibleTargets.isEmpty()) { @@ -271,9 +275,9 @@ class ConversationRepository( } @Throws(IOException::class, GroupNotAMemberException::class, UndeliverableMessageException::class) - fun sendEndPoll(group: Recipient, message: OutgoingMessage, destinations: List, messageId: Long): List { - val groupId = group.requireGroupId().requireV2() - val groupRecord: GroupRecord? = SignalDatabase.groups.getGroup(group.requireGroupId()).getOrNull() + fun sendEndPoll(threadRecipient: Recipient, message: OutgoingMessage, destinations: List, isSelf: Boolean, messageId: Long): List { + val groupId = if (threadRecipient.isPushV2Group) threadRecipient.requireGroupId().requireV2() else null + val groupRecord: GroupRecord? = if (threadRecipient.isPushV2Group) SignalDatabase.groups.getGroup(threadRecipient.requireGroupId()).getOrNull() else null if (groupRecord != null && groupRecord.isAnnouncementGroup && !groupRecord.isAdmin(Recipient.self())) { throw UndeliverableMessageException("Non-admins cannot send messages in announcement groups!") @@ -281,29 +285,35 @@ class ConversationRepository( val builder = newBuilder() - GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId) + if (groupId != null) { + GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId) + } val sentTime = System.currentTimeMillis() - val groupMessage = builder + val message = builder .withTimestamp(sentTime) .withExpiration((message.expiresIn / 1000).toInt()) .withProfileKey(ProfileKeyUtil.getSelfProfileKey().serialize()) .withPollTerminate(SignalServiceDataMessage.PollTerminate(message.messageExtras!!.pollTerminate!!.targetTimestamp)) .build() - return GroupSendUtil.sendResendableDataMessage( - applicationContext, - groupId, - null, - destinations, - false, - ContentHint.RESENDABLE, - MessageId(messageId), - groupMessage, - true, - false, - null - ) { System.currentTimeMillis() - sentTime > POLL_TERMINATE_TIMEOUT.inWholeMilliseconds } + return if (isSelf) { + listOf(AppDependencies.signalServiceMessageSender.sendSyncMessage(message)) + } else { + GroupSendUtil.sendResendableDataMessage( + applicationContext, + groupId, + null, + destinations, + false, + ContentHint.RESENDABLE, + MessageId(messageId), + message, + true, + false, + null + ) { System.currentTimeMillis() - sentTime > POLL_TERMINATE_TIMEOUT.inWholeMilliseconds } + } } fun getPinnedMessages(threadId: Long): List { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt index 690502b0a3..f8815716a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt @@ -67,6 +67,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.compose.ComposeDialogFragment import org.thoughtcrime.securesms.polls.Poll +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.ViewUtil import kotlin.time.Duration.Companion.milliseconds @@ -78,6 +79,7 @@ class CreatePollFragment : ComposeDialogFragment() { companion object { private val TAG = Log.tag(CreatePollFragment::class) + val MAX_QUESTION_CHARACTER_LENGTH = if (RemoteConfig.pollsV2) 200 else 100 const val MAX_CHARACTER_LENGTH = 100 const val MAX_OPTIONS = 10 const val MIN_OPTIONS = 2 @@ -222,7 +224,7 @@ private fun CreatePollScreen( TextFieldWithCountdown( value = question, label = { Text(text = stringResource(R.string.CreatePollFragment__ask_a_question)) }, - onValueChange = { question = it.substring(0, minOf(it.length, CreatePollFragment.MAX_CHARACTER_LENGTH)) }, + onValueChange = { question = it.substring(0, minOf(it.length, CreatePollFragment.MAX_QUESTION_CHARACTER_LENGTH)) }, keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), colors = TextFieldDefaults.colors( unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, @@ -232,6 +234,7 @@ private fun CreatePollScreen( .fillMaxWidth() .onFocusChanged { focusState -> if (focusState.isFocused) focusedOption = -1 } .focusRequester(focusRequester), + maxCharacterLength = CreatePollFragment.MAX_QUESTION_CHARACTER_LENGTH, countdownThreshold = CreatePollFragment.CHARACTER_COUNTDOWN_THRESHOLD ) @@ -266,6 +269,7 @@ private fun CreatePollScreen( tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, + maxCharacterLength = CreatePollFragment.MAX_CHARACTER_LENGTH, countdownThreshold = CreatePollFragment.CHARACTER_COUNTDOWN_THRESHOLD ) } @@ -316,9 +320,10 @@ private fun TextFieldWithCountdown( colors: TextFieldColors, modifier: Modifier, trailingIcon: @Composable () -> Unit = {}, + maxCharacterLength: Int, countdownThreshold: Int ) { - val charactersRemaining = CreatePollFragment.MAX_CHARACTER_LENGTH - value.length + val charactersRemaining = maxCharacterLength - value.length val displayCountdown = charactersRemaining <= countdownThreshold Box(modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 16.dp)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt index b43522c6eb..5ee9258376 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt @@ -131,7 +131,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ private fun updateButtonsAvailable(recipient: Recipient) { val paymentsValues = SignalStore.payments val isPaymentsAvailable = paymentsValues.paymentsAvailability.isSendAllowed && !recipient.isSelf && !recipient.isGroup && recipient.isRegistered - val isPollsAvailable = recipient.isPushV2Group && RemoteConfig.polls + val isPollsAvailable = recipient.isPushV2Group || RemoteConfig.pollsV2 if (!isPaymentsAvailable && !isPollsAvailable) { attachmentKeyboardView.filterAttachmentKeyboardButtons(removePaymentFilter.and(removePollFilter)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index ca5a113eff..fb1d82f483 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -286,6 +286,8 @@ public class IndividualSendJob extends PushSendJob { SignalServiceDataMessage.GiftBadge giftBadge = getGiftBadgeFor(message); SignalServiceDataMessage.Payment payment = getPayment(message); List bodyRanges = getBodyRanges(message); + SignalServiceDataMessage.PollCreate pollCreate = getPollCreate(message); + SignalServiceDataMessage.PollTerminate pollTerminate = getPollTerminate(message); SignalServiceDataMessage.PinnedMessage pinnedMessage = getPinnedMessage(message); SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder() .withBody(message.getBody()) @@ -303,6 +305,8 @@ public class IndividualSendJob extends PushSendJob { .asEndSessionMessage(message.isEndSession()) .withPayment(payment) .withBodyRanges(bodyRanges) + .withPollCreate(pollCreate) + .withPollTerminate(pollTerminate) .withPinnedMessage(pinnedMessage); if (message.getParentStoryId() != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt index b7e27762ae..d926d1735b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PollVoteJob.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint @@ -49,7 +50,11 @@ class PollVoteJob( return null } - val recipients = conversationRecipient.participantIds.filter { it != Recipient.self().id }.map { it.toLong() } + val recipients = if (conversationRecipient.isGroup) { + conversationRecipient.participantIds.filter { it != Recipient.self().id }.map { it.toLong() } + } else { + listOf(conversationRecipient.id.toLong()) + } return PollVoteJob( messageId = messageId, @@ -108,7 +113,7 @@ class PollVoteJob( val targetSentTimestamp = message.dateSent - val recipients = Recipient.resolvedList(recipientIds.filter { it != Recipient.self().id.toLong() }.map { RecipientId.from(it) }) + val recipients = Recipient.resolvedList(recipientIds.map { RecipientId.from(it) }) val registered = RecipientUtil.getEligibleForSending(recipients) val unregistered = recipients - registered.toSet() val completions: List = deliver(conversationRecipient, registered, targetAuthor, targetSentTimestamp, poll) @@ -140,15 +145,18 @@ class PollVoteJob( ) ) - GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush()) + if (conversationRecipient.isPushV2Group) { + GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush()) + } val dataMessage = dataMessageBuilder.build() + val nonSelfDestinations = destinations.filter { !it.isSelf } val results = GroupSendUtil.sendResendableDataMessage( context, conversationRecipient.groupId.map { obj: GroupId -> obj.requireV2() }.orElse(null), null, - destinations, + nonSelfDestinations, false, ContentHint.RESENDABLE, MessageId(messageId), @@ -159,6 +167,10 @@ class PollVoteJob( null ) + if (conversationRecipient.isSelf) { + results.add(AppDependencies.signalServiceMessageSender.sendSyncMessage(dataMessage)) + } + val groupResult = GroupSendJobHelper.getCompletedSends(destinations, results) for (unregistered in groupResult.unregistered) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 0dc626c98d..e229774b8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.messages.StorySendUtil; import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; -import org.thoughtcrime.securesms.polls.Poll; import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -285,8 +284,8 @@ public final class PushGroupSendJob extends PushSendJob { List previews = getPreviewsFor(message); List mentions = getMentionsFor(message.getMentions()); List bodyRanges = getBodyRanges(message); - Optional pollCreate = getPollCreate(message); - Optional pollTerminate = getPollTerminate(message); + SignalServiceDataMessage.PollCreate pollCreate = getPollCreate(message); + SignalServiceDataMessage.PollTerminate pollTerminate = getPollTerminate(message); SignalServiceDataMessage.PinnedMessage pinnedMessage = getPinnedMessage(message); List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List attachmentPointers = getAttachmentPointersFor(attachments); @@ -371,8 +370,8 @@ public final class PushGroupSendJob extends PushSendJob { .withPreviews(previews) .withMentions(mentions) .withBodyRanges(bodyRanges) - .withPollCreate(pollCreate.orElse(null)) - .withPollTerminate(pollTerminate.orElse(null)) + .withPollCreate(pollCreate) + .withPollTerminate(pollTerminate) .withPinnedMessage(pinnedMessage); if (message.getParentStoryId() != null) { @@ -419,23 +418,6 @@ public final class PushGroupSendJob extends PushSendJob { } } - private Optional getPollCreate(OutgoingMessage message) { - Poll poll = message.getPoll(); - if (poll == null) { - return Optional.empty(); - } - - return Optional.of(new SignalServiceDataMessage.PollCreate(poll.getQuestion(), poll.getAllowMultipleVotes(), poll.getPollOptions())); - } - - private Optional getPollTerminate(OutgoingMessage message) { - if (message.getMessageExtras() == null || message.getMessageExtras().pollTerminate == null) { - return Optional.empty(); - } - - return Optional.of(new SignalServiceDataMessage.PollTerminate(message.getMessageExtras().pollTerminate.targetTimestamp)); - } - public static long getMessageId(@Nullable byte[] serializedData) { JsonJobData data = JsonJobData.deserialize(serializedData); return data.getLong(KEY_MESSAGE_ID); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 4a964ed6e4..632b0952d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.notifications.v2.ConversationId; +import org.thoughtcrime.securesms.polls.Poll; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -486,6 +487,23 @@ public abstract class PushSendJob extends SendJob { return getBodyRanges(message.getBodyRanges()); } + protected @Nullable SignalServiceDataMessage.PollCreate getPollCreate(OutgoingMessage message) { + Poll poll = message.getPoll(); + if (poll == null) { + return null; + } + + return new SignalServiceDataMessage.PollCreate(poll.getQuestion(), poll.getAllowMultipleVotes(), poll.getPollOptions()); + } + + protected @Nullable SignalServiceDataMessage.PollTerminate getPollTerminate(OutgoingMessage message) { + if (message.getMessageExtras() == null || message.getMessageExtras().pollTerminate == null) { + return null; + } + + return new SignalServiceDataMessage.PollTerminate(message.getMessageExtras().pollTerminate.targetTimestamp); + } + protected @Nullable List getBodyRanges(@Nullable BodyRangeList bodyRanges) { if (bodyRanges == null || bodyRanges.ranges.size() == 0) { return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index 1ef6ae4404..0747ba02b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -124,6 +124,7 @@ import kotlin.time.Duration.Companion.seconds object DataMessageProcessor { private const val BODY_RANGE_PROCESSING_LIMIT = 250 + private const val POLL_QUESTION_CHARACTER_LIMIT = 200 private const val POLL_CHARACTER_LIMIT = 100 private const val POLL_OPTIONS_LIMIT = 10 @@ -1066,28 +1067,23 @@ object DataMessageProcessor { groupId: GroupId.V2?, receivedTime: Long ): InsertResult? { - if (!RemoteConfig.receivePolls) { - log(envelope.timestamp!!, "Poll creation not allowed due to remote config.") - return null - } - log(envelope.timestamp!!, "Handle poll creation") val poll: DataMessage.PollCreate = message.pollCreate!! handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime) - if (groupId == null) { - warn(envelope.timestamp!!, "[handlePollCreate] Polls can only be sent to groups. author: $senderRecipient") + if (groupId != null) { + val groupRecord = SignalDatabase.groups.getGroup(groupId).orNull() + if (groupRecord != null && !groupRecord.members.contains(senderRecipient.id)) { + warn(envelope.timestamp!!, "[handlePollCreate] Poll author is not in the group. author $senderRecipient") + return null + } + } else if (senderRecipient.id != threadRecipient.id && senderRecipient.id != Recipient.self().id) { + warn(envelope.timestamp!!, "[handlePollCreate] Sender is not a part of the 1:1 thread!") return null } - val groupRecord = SignalDatabase.groups.getGroup(groupId).orNull() - if (groupRecord == null || !groupRecord.members.contains(senderRecipient.id)) { - warn(envelope.timestamp!!, "[handlePollCreate] Poll author is not in the group. author $senderRecipient") - return null - } - - if (poll.question == null || poll.question!!.isEmpty() || poll.question!!.length > POLL_CHARACTER_LIMIT) { + if (poll.question == null || poll.question!!.isEmpty() || poll.question!!.length > POLL_QUESTION_CHARACTER_LIMIT) { warn(envelope.timestamp!!, "[handlePollCreate] Poll question is invalid.") return null } @@ -1136,11 +1132,6 @@ object DataMessageProcessor { groupId: GroupId.V2?, receivedTime: Long ): InsertResult? { - if (!RemoteConfig.receivePolls) { - log(envelope.timestamp!!, "Poll terminate not allowed due to remote config.") - return null - } - val pollTerminate: DataMessage.PollTerminate = message.pollTerminate!! val targetSentTimestamp = pollTerminate.targetSentTimestamp!! @@ -1189,11 +1180,6 @@ object DataMessageProcessor { senderRecipient: Recipient, earlyMessageCacheEntry: EarlyMessageCacheEntry? ): MessageId? { - if (!RemoteConfig.receivePolls) { - log(envelope.timestamp!!, "Poll vote not allowed due to remote config.") - return null - } - val pollVote: DataMessage.PollVote = message.pollVote!! val targetSentTimestamp = pollVote.targetSentTimestamp!! @@ -1578,14 +1564,12 @@ object DataMessageProcessor { } val groupRecord = SignalDatabase.groups.getGroup(targetThread.recipient.id).orNull() - if (groupRecord == null) { - warn(envelope.timestamp!!, "[handlePollValidation] Target thread needs to be a group. timestamp: $targetSentTimestamp author: ${targetAuthor.id}") - return null - } - - if (!groupRecord.members.contains(senderRecipient.id)) { + if (groupRecord != null && !groupRecord.members.contains(senderRecipient.id)) { warn(envelope.timestamp!!, "[handlePollValidation] Sender is not in the group. timestamp: $targetSentTimestamp author: ${targetAuthor.id}") return null + } else if (groupRecord == null && senderRecipient.id != targetThread.recipient.id && senderRecipient.id != Recipient.self().id) { + warn(envelope.timestamp!!, "[handlePollValidation] Sender is not a part of the 1:1 thread!") + return null } return MessageId(targetMessage.id) 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 a1d13e83c7..0a11704c1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1744,18 +1744,9 @@ object SyncMessageProcessor { sent: Sent, senderRecipient: Recipient ): Long { - if (!RemoteConfig.receivePolls) { - log(envelope.timestamp!!, "Sync poll create not allowed due to remote config.") - } - log(envelope.timestamp!!, "Synchronize sent poll creation message.") val recipient = getSyncMessageDestination(sent) - if (!recipient.isGroup) { - warn(envelope.timestamp!!, "Poll creation messages should only be synced in groups. Dropping.") - return -1 - } - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) val expiresInMillis = message.expireTimerDuration.inWholeMilliseconds @@ -1777,8 +1768,12 @@ object SyncMessageProcessor { question = poll.question!! ) - val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId - updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId()) + val receiptStatus = if (recipient.isGroup) GroupReceiptTable.STATUS_UNKNOWN else GroupReceiptTable.STATUS_UNDELIVERED + val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, receiptStatus, null).messageId + + if (recipient.isGroup) { + updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId()) + } log(envelope.timestamp!!, "Inserted sync poll create message as messageId $messageId") @@ -1799,18 +1794,9 @@ object SyncMessageProcessor { senderRecipient: Recipient, earlyMessageCacheEntry: EarlyMessageCacheEntry? ): Long { - if (!RemoteConfig.receivePolls) { - log(envelope.timestamp!!, "Sync poll end not allowed due to remote config.") - } - log(envelope.timestamp!!, "Synchronize sent poll terminate message") val recipient = getSyncMessageDestination(sent) - if (!recipient.isGroup) { - warn(envelope.timestamp!!, "Poll termination messages should only be synced in groups. Dropping.") - return -1 - } - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) val expiresInMillis = message.expireTimerDuration.inWholeMilliseconds @@ -1847,7 +1833,8 @@ object SyncMessageProcessor { ) ) - val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId + val receiptStatus = if (recipient.isGroup) GroupReceiptTable.STATUS_UNKNOWN else GroupReceiptTable.STATUS_UNDELIVERED + val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, receiptStatus, null).messageId SignalDatabase.messages.markAsSent(messageId, true) log(envelope.timestamp!!, "Inserted sync poll end message as messageId $messageId") diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 1e6e8273f4..0f600a0f24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -265,12 +265,11 @@ public class MessageSender { long messageId = insertResult.getMessageId(); if (!recipient.isPushV2Group()) { - Log.w(TAG, "Can only send polls to groups."); - return threadId; + SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId); + } else { + SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId); } - SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId); - sendMessageInternal(context, recipient, sendType, messageId, insertResult.getQuoteAttachmentId(), Collections.emptyList()); onMessageSent(); SignalDatabase.threads().update(allocatedThreadId, true, true); 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 a12e6b9779..f2b0f9edbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1166,14 +1166,6 @@ object RemoteConfig { hotSwappable = true ) - @JvmStatic - @get:JvmName("polls") - val polls: Boolean by remoteBoolean( - key = "android.polls.2", - defaultValue = false, - hotSwappable = true - ) - /** Whether or not to send over binary service ids (alongside string service ids). */ @JvmStatic @get:JvmName("useBinaryId") @@ -1183,14 +1175,6 @@ object RemoteConfig { hotSwappable = false ) - @JvmStatic - @get:JvmName("receivePolls") - val receivePolls: Boolean by remoteBoolean( - key = "android.receivePolls", - defaultValue = false, - hotSwappable = true - ) - @JvmStatic @get:JvmName("backupsBetaMegaphone") val backupsBetaMegaphone: Boolean by remoteBoolean( @@ -1238,5 +1222,16 @@ object RemoteConfig { defaultValue = "*:10000", hotSwappable = true ) + + /** + * Whether or not to allow 1:1 polls and a higher character limit for questions + */ + @JvmStatic + @get:JvmName("pollsV2") + val pollsV2: Boolean by remoteBoolean( + key = "android.pollsV2", + defaultValue = false, + hotSwappable = true + ) // endregion } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 529747ead7..4476ba1e49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8941,7 +8941,7 @@ End poll? - Group members will no longer be able to vote in this poll. + Members of this chat will no longer be able to vote in this poll. Waiting for network… diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c25dff3e04..5263e8e8d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ androidx-navigation = "2.8.5" androidx-navigation3-core = "1.0.0" androidx-window = "1.3.0" glide = "4.15.1" -libsignal-client = "0.86.8" +libsignal-client = "0.86.9" mp4parser = "1.9.39" accompanist = "0.28.0" nanohttpd = "2.3.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 038fb4b16f..98085d321f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -15400,20 +15400,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + - - - + + + - - + + diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt index 124084ad12..9376b490c8 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt @@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.util.Util */ object EnvelopeContentValidator { + private const val MAX_POLL_QUESTION_CHARACTER_LENGTH = 200 private const val MAX_POLL_CHARACTER_LENGTH = 100 private const val MIN_POLL_OPTIONS = 2 @@ -167,7 +168,7 @@ object EnvelopeContentValidator { } private fun DataMessage.PollCreate.hasInvalidPollQuestion(): Boolean { - return this.question.isNullOrBlank() || this.question.length > MAX_POLL_CHARACTER_LENGTH + return this.question.isNullOrBlank() || this.question.length > MAX_POLL_QUESTION_CHARACTER_LENGTH } private fun DataMessage.PollCreate.hasInvalidPollOptions(): Boolean { diff --git a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt index d8d4523649..57f080829c 100644 --- a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt +++ b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt @@ -50,11 +50,11 @@ class EnvelopeContentValidatorTest { } @Test - fun `validate - ensure polls with a question exceeding 100 characters are marked invalid`() { + fun `validate - ensure polls with a question exceeding 200 characters are marked invalid`() { val content = Content( dataMessage = DataMessage( pollCreate = DataMessage.PollCreate( - question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz", + question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz", options = listOf("option1", "option2"), allowMultiple = true )