mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Release polls behind feature flag.
This commit is contained in:
@@ -1231,6 +1231,32 @@ public class SignalServiceMessageSender {
|
||||
builder.bodyRanges(bodyRanges);
|
||||
}
|
||||
|
||||
if (message.getPollCreate().isPresent()) {
|
||||
SignalServiceDataMessage.PollCreate pollCreate = message.getPollCreate().get();
|
||||
|
||||
builder.pollCreate(new DataMessage.PollCreate.Builder()
|
||||
.question(pollCreate.getQuestion())
|
||||
.allowMultiple(pollCreate.getAllowMultiple())
|
||||
.options(pollCreate.getOptions()).build());
|
||||
}
|
||||
|
||||
if (message.getPollVote().isPresent()) {
|
||||
SignalServiceDataMessage.PollVote pollVote = message.getPollVote().get();
|
||||
builder.pollVote(new DataMessage.PollVote.Builder()
|
||||
.targetSentTimestamp(pollVote.getTargetSentTimestamp())
|
||||
.targetAuthorAciBinary(pollVote.getTargetAuthor().toByteString())
|
||||
.voteCount(pollVote.getVoteCount())
|
||||
.optionIndexes(pollVote.getOptionIndexes())
|
||||
.build());
|
||||
}
|
||||
|
||||
if (message.getPollTerminate().isPresent()) {
|
||||
SignalServiceDataMessage.PollTerminate pollTerminate = message.getPollTerminate().get();
|
||||
builder.pollTerminate(new DataMessage.PollTerminate.Builder()
|
||||
.targetSentTimestamp(pollTerminate.getTargetSentTimestamp())
|
||||
.build());
|
||||
}
|
||||
|
||||
builder.timestamp(message.getTimestamp());
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -28,6 +28,9 @@ import org.whispersystems.signalservice.internal.push.TypingMessage
|
||||
*/
|
||||
object EnvelopeContentValidator {
|
||||
|
||||
private const val MAX_POLL_CHARACTER_LENGTH = 100
|
||||
private const val MIN_POLL_OPTIONS = 2
|
||||
|
||||
fun validate(envelope: Envelope, content: Content, localAci: ACI): Result {
|
||||
if (envelope.type == Envelope.Type.PLAINTEXT_CONTENT) {
|
||||
validatePlaintextContent(content)?.let { return it }
|
||||
@@ -138,9 +141,29 @@ object EnvelopeContentValidator {
|
||||
validateGroupContextV2(dataMessage.groupV2, "[DataMessage]")?.let { return it }
|
||||
}
|
||||
|
||||
if (dataMessage.pollCreate != null && (dataMessage.pollCreate.hasInvalidPollQuestion() || dataMessage.pollCreate.hasInvalidPollOptions() || dataMessage.pollCreate.allowMultiple == null)) {
|
||||
return Result.Invalid("[DataMessage] Invalid poll create!")
|
||||
}
|
||||
|
||||
if (dataMessage.pollTerminate != null && dataMessage.pollTerminate.targetSentTimestamp == null) {
|
||||
return Result.Invalid("[DataMessage] Invalid poll terminate!")
|
||||
}
|
||||
|
||||
if (dataMessage.pollVote != null && (dataMessage.pollVote.targetAuthorAciBinary.isNullOrInvalidAci() || dataMessage.pollVote.targetSentTimestamp == null || dataMessage.pollVote.voteCount == null)) {
|
||||
return Result.Invalid("[DataMessage] Invalid poll vote!")
|
||||
}
|
||||
|
||||
return Result.Valid
|
||||
}
|
||||
|
||||
private fun DataMessage.PollCreate.hasInvalidPollQuestion(): Boolean {
|
||||
return this.question.isNullOrBlank() || this.question.length > MAX_POLL_CHARACTER_LENGTH
|
||||
}
|
||||
|
||||
private fun DataMessage.PollCreate.hasInvalidPollOptions(): Boolean {
|
||||
return this.options.size < MIN_POLL_OPTIONS || this.options.any { option -> option.length > MAX_POLL_CHARACTER_LENGTH }
|
||||
}
|
||||
|
||||
private fun validateSyncMessage(envelope: Envelope, syncMessage: SyncMessage, localAci: ACI): Result {
|
||||
// Source serviceId was already determined to be a valid serviceId in general
|
||||
val sourceServiceId = ServiceId.parseOrThrow(envelope.sourceServiceId!!)
|
||||
@@ -349,6 +372,11 @@ object EnvelopeContentValidator {
|
||||
return parsed == null || parsed.isUnknown
|
||||
}
|
||||
|
||||
private fun ByteString?.isNullOrInvalidAci(): Boolean {
|
||||
val parsed = this?.let { ACI.parseOrNull(this) }
|
||||
return parsed == null || parsed.isUnknown
|
||||
}
|
||||
|
||||
private fun ByteString?.isNullOrInvalidPni(): Boolean {
|
||||
val parsed = ServiceId.PNI.parseOrNull(this?.toByteArray())
|
||||
return parsed == null || parsed.isUnknown
|
||||
|
||||
@@ -50,7 +50,10 @@ class SignalServiceDataMessage private constructor(
|
||||
val payment: Optional<Payment>,
|
||||
val storyContext: Optional<StoryContext>,
|
||||
val giftBadge: Optional<GiftBadge>,
|
||||
val bodyRanges: Optional<List<BodyRange>>
|
||||
val bodyRanges: Optional<List<BodyRange>>,
|
||||
val pollCreate: Optional<PollCreate>,
|
||||
val pollVote: Optional<PollVote>,
|
||||
val pollTerminate: Optional<PollTerminate>
|
||||
) {
|
||||
val isActivatePaymentsRequest: Boolean = payment.map { it.isActivationRequest }.orElse(false)
|
||||
val isPaymentsActivated: Boolean = payment.map { it.isActivation }.orElse(false)
|
||||
@@ -68,7 +71,10 @@ class SignalServiceDataMessage private constructor(
|
||||
this.mentions.isPresent ||
|
||||
this.sticker.isPresent ||
|
||||
this.reaction.isPresent ||
|
||||
this.remoteDelete.isPresent
|
||||
this.remoteDelete.isPresent ||
|
||||
this.pollCreate.isPresent ||
|
||||
this.pollVote.isPresent ||
|
||||
this.pollTerminate.isPresent
|
||||
|
||||
val isGroupV2Update: Boolean = groupContext.isPresent && groupContext.get().hasSignedGroupChange() && !hasRenderableContent
|
||||
val isEmptyGroupV2Message: Boolean = isGroupV2Message && !isGroupV2Update && !hasRenderableContent
|
||||
@@ -97,6 +103,9 @@ class SignalServiceDataMessage private constructor(
|
||||
private var storyContext: StoryContext? = null
|
||||
private var giftBadge: GiftBadge? = null
|
||||
private var bodyRanges: MutableList<BodyRange> = LinkedList<BodyRange>()
|
||||
private var pollCreate: PollCreate? = null
|
||||
private var pollVote: PollVote? = null
|
||||
private var pollTerminate: PollTerminate? = null
|
||||
|
||||
fun withTimestamp(timestamp: Long): Builder {
|
||||
this.timestamp = timestamp
|
||||
@@ -220,6 +229,21 @@ class SignalServiceDataMessage private constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
fun withPollCreate(pollCreate: PollCreate?): Builder {
|
||||
this.pollCreate = pollCreate
|
||||
return this
|
||||
}
|
||||
|
||||
fun withPollVote(pollVote: PollVote?): Builder {
|
||||
this.pollVote = pollVote
|
||||
return this
|
||||
}
|
||||
|
||||
fun withPollTerminate(pollTerminate: PollTerminate?): Builder {
|
||||
this.pollTerminate = pollTerminate
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): SignalServiceDataMessage {
|
||||
if (timestamp == 0L) {
|
||||
timestamp = System.currentTimeMillis()
|
||||
@@ -248,7 +272,10 @@ class SignalServiceDataMessage private constructor(
|
||||
payment = payment.asOptional(),
|
||||
storyContext = storyContext.asOptional(),
|
||||
giftBadge = giftBadge.asOptional(),
|
||||
bodyRanges = bodyRanges.asOptional()
|
||||
bodyRanges = bodyRanges.asOptional(),
|
||||
pollCreate = pollCreate.asOptional(),
|
||||
pollVote = pollVote.asOptional(),
|
||||
pollTerminate = pollTerminate.asOptional()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -291,6 +318,9 @@ class SignalServiceDataMessage private constructor(
|
||||
}
|
||||
data class StoryContext(val authorServiceId: ServiceId, val sentTimestamp: Long)
|
||||
data class GiftBadge(val receiptCredentialPresentation: ReceiptCredentialPresentation)
|
||||
data class PollCreate(val question: String, val allowMultiple: Boolean, val options: List<String>)
|
||||
data class PollVote(val targetAuthor: ServiceId, val targetSentTimestamp: Long, val optionIndexes: List<Int>, val voteCount: Int)
|
||||
data class PollTerminate(val targetSentTimestamp: Long)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
||||
@@ -302,6 +302,7 @@ message DataMessage {
|
||||
CDN_SELECTOR_ATTACHMENTS = 5;
|
||||
MENTIONS = 6;
|
||||
PAYMENTS = 7;
|
||||
POLLS = 8;
|
||||
CURRENT = 7;
|
||||
}
|
||||
|
||||
@@ -309,6 +310,23 @@ message DataMessage {
|
||||
optional bytes receiptCredentialPresentation = 1;
|
||||
}
|
||||
|
||||
message PollCreate {
|
||||
optional string question = 1;
|
||||
optional bool allowMultiple = 2;
|
||||
repeated string options = 3;
|
||||
}
|
||||
|
||||
message PollTerminate {
|
||||
optional uint64 targetSentTimestamp = 1;
|
||||
}
|
||||
|
||||
message PollVote {
|
||||
optional bytes targetAuthorAciBinary = 1;
|
||||
optional uint64 targetSentTimestamp = 2;
|
||||
repeated uint32 optionIndexes = 3; // must be in the range [0, options.length) from the PollCreate
|
||||
optional uint32 voteCount = 4; // increment this by 1 each time you vote on a given poll
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
repeated AttachmentPointer attachments = 2;
|
||||
reserved /*groupV1*/ 3;
|
||||
@@ -331,7 +349,10 @@ message DataMessage {
|
||||
optional Payment payment = 20;
|
||||
optional StoryContext storyContext = 21;
|
||||
optional GiftBadge giftBadge = 22;
|
||||
// NEXT ID: 24
|
||||
optional PollCreate pollCreate = 24;
|
||||
optional PollTerminate pollTerminate = 25;
|
||||
optional PollVote pollVote = 26;
|
||||
// NEXT ID: 27
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.messages
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Test
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
@@ -32,4 +33,108 @@ class EnvelopeContentValidatorTest {
|
||||
val result = EnvelopeContentValidator.validate(envelope, content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure polls without a question are marked invalid`() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollCreate = DataMessage.PollCreate(
|
||||
options = listOf("option1", "option2"),
|
||||
allowMultiple = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure polls with a question exceeding 100 characters are marked invalid`() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollCreate = DataMessage.PollCreate(
|
||||
question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz",
|
||||
options = listOf("option1", "option2"),
|
||||
allowMultiple = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure polls without at least two options are marked invalid`() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollCreate = DataMessage.PollCreate(
|
||||
question = "how are you?",
|
||||
options = listOf("option1"),
|
||||
allowMultiple = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure poll options that exceed 100 characters are marked invalid `() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollCreate = DataMessage.PollCreate(
|
||||
question = "how are you",
|
||||
options = listOf("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz", "option2"),
|
||||
allowMultiple = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure polls without an explicit allow multiple votes option are marked invalid `() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollCreate = DataMessage.PollCreate(
|
||||
question = "how are you",
|
||||
options = listOf("option1", "option2")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure poll terminate without timestamps are marked invalid `() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollTerminate = DataMessage.PollTerminate()
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate - ensure poll votes without a valid aci are marked invalid`() {
|
||||
val content = Content(
|
||||
dataMessage = DataMessage(
|
||||
pollVote = DataMessage.PollVote(
|
||||
targetAuthorAciBinary = "bad".toByteArray().toByteString()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
|
||||
assert(result is EnvelopeContentValidator.Result.Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user