Add sync message support for edit message.

This commit is contained in:
Clark
2023-04-19 12:19:29 -04:00
committed by Cody Henthorne
parent 85787ba1df
commit 5ddd7cdb9e
8 changed files with 495 additions and 65 deletions

View File

@@ -0,0 +1,250 @@
package org.thoughtcrime.securesms.messages
import android.database.Cursor
import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.signal.core.util.readToList
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.toBodyRangeList
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableUtils
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
import kotlin.time.Duration.Companion.seconds
@RunWith(AndroidJUnit4::class)
class EditMessageSyncProcessorTest {
companion object {
private val IGNORE_MESSAGE_COLUMNS = listOf(
MessageTable.DATE_RECEIVED,
MessageTable.NOTIFIED_TIMESTAMP,
MessageTable.REACTIONS_LAST_SEEN,
MessageTable.NOTIFIED
)
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
AttachmentTable.UNIQUE_ID,
AttachmentTable.TRANSFER_FILE
)
}
@get:Rule
val harness = SignalActivityRule()
private lateinit var processorV2: MessageContentProcessorV2
private lateinit var testResult: TestResults
private var envelopeTimestamp: Long = 0
@Before
fun setup() {
processorV2 = MessageContentProcessorV2(harness.context)
envelopeTimestamp = System.currentTimeMillis()
testResult = TestResults()
}
@Test
fun textMessage() {
var originalTimestamp = envelopeTimestamp + 200
for (i in 1..10) {
originalTimestamp += 400
val toRecipient = Recipient.resolved(harness.others[0])
val content = MessageContentFuzzer.fuzzTextMessage()
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationUuid(metadata.destinationServiceId.toString())
.setTimestamp(originalTimestamp)
.setExpirationStartTimestamp(originalTimestamp)
.setMessage(content.dataMessage)
)
).build()
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
val syncTextMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(originalTimestamp),
content = syncContent,
metadata = metadata,
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(originalTimestamp)
)
val editTimestamp = originalTimestamp + 200
val editedContent = MessageContentFuzzer.fuzzTextMessage()
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationUuid(metadata.destinationServiceId.toString())
.setTimestamp(editTimestamp)
.setExpirationStartTimestamp(editTimestamp)
.setEditMessage(
EditMessage.newBuilder()
.setDataMessage(editedContent.dataMessage)
.setTargetSentTimestamp(originalTimestamp)
)
)
).build()
val syncEditMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(editTimestamp),
content = editSyncContent,
metadata = metadata,
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(editTimestamp)
)
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
val originalTextMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = originalTimestamp,
body = content.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(originalMessageId, true)
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
}
val editMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = editTimestamp,
body = editedContent.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
messageToEdit = originalMessageId
)
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(editMessageId, true)
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
}
testResult.collectLocal()
testResult.assert()
}
}
private inner class TestResults {
private lateinit var localMessages: List<List<Pair<String, String?>>>
private lateinit var localAttachments: List<List<Pair<String, String?>>>
private lateinit var syncMessages: List<List<Pair<String, String?>>>
private lateinit var syncAttachments: List<List<Pair<String, String?>>>
fun collectLocal() {
harness.inMemoryLogger.clear()
localMessages = dumpMessages()
localAttachments = dumpAttachments()
cleanup()
}
fun runSync(messages: List<TestMessage>) {
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
if (content.hasSyncMessage()) {
processorV2.process(
envelope,
content,
metadata,
serverDeliveredTimestamp,
false
)
ThreadUtil.sleep(1)
}
}
harness.inMemoryLogger.clear()
syncMessages = dumpMessages()
syncAttachments = dumpAttachments()
cleanup()
}
fun cleanup() {
SignalDatabase.rawDatabase.withinTransaction { db ->
SignalDatabase.threads.deleteAllConversations()
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
}
}
fun assert() {
syncMessages.zip(localMessages)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
syncAttachments.zip(localAttachments)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
}
private fun dumpMessages(): List<List<Pair<String, String?>>> {
return dumpTable(MessageTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
return dumpTable(AttachmentTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableUtils.typeColumnToString(cursor.getLong(index))
}
column to data
}
map
}
}
}
}

View File

@@ -13,8 +13,6 @@ import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.MessageTypes.isOutgoingMessageType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -23,6 +21,7 @@ import org.thoughtcrime.securesms.testing.InMemoryLogger
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableUtils
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
@@ -279,7 +278,7 @@ class MessageContentProcessorTestV2 {
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = typeColumnToString(cursor.getLong(index))
data = MessageTableUtils.typeColumnToString(cursor.getLong(index))
}
column to data
@@ -311,64 +310,4 @@ class MessageContentProcessorTestV2 {
return SignalServiceContent.createFromProto(contentProto)!!
}
fun typeColumnToString(type: Long): String {
return """
isOutgoingMessageType:${isOutgoingMessageType(type)}
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
isDecryptInProgressType:${type and 0x40000000 != 0L}
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "")
}
}

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.util
import org.thoughtcrime.securesms.database.MessageTypes
object MessageTableUtils {
fun typeColumnToString(type: Long): String {
return """
isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)}
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
isDecryptInProgressType:${type and 0x40000000 != 0L}
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "")
}
}