Add support for versioned expiration timers.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
Cody Henthorne 2024-08-27 07:41:35 -04:00 committed by Nicholas Tinsley
parent 4152294b57
commit 1f196f74ff
43 changed files with 392 additions and 139 deletions

View file

@ -77,7 +77,7 @@ class EditMessageSyncProcessorTest {
.build() .build()
).build() ).build()
).build() ).build()
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0) SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0, content.dataMessage?.expireTimerVersion ?: 1)
val syncTextMessage = TestMessage( val syncTextMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(originalTimestamp), envelope = MessageContentFuzzer.envelope(originalTimestamp),
content = syncContent, content = syncContent,
@ -112,7 +112,7 @@ class EditMessageSyncProcessorTest {
testResult.runSync(listOf(syncTextMessage, syncEditMessage)) testResult.runSync(listOf(syncTextMessage, syncEditMessage))
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000) SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000, content.dataMessage?.expireTimerVersion ?: 1)
val originalTextMessage = OutgoingMessage( val originalTextMessage = OutgoingMessage(
threadRecipient = toRecipient, threadRecipient = toRecipient,
sentTimeMillis = originalTimestamp, sentTimeMillis = originalTimestamp,

View file

@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false)) SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci) SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair() val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()

View file

@ -96,6 +96,7 @@ class ConversationElementGenerator {
0, 0,
0, 0,
0, 0,
0,
false, false,
true, true,
null, null,

View file

@ -11,7 +11,8 @@ object AppCapabilities {
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities { fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
return AccountAttributes.Capabilities( return AccountAttributes.Capabilities(
storage = storageCapable, storage = storageCapable,
deleteSync = true deleteSync = true,
expireTimerVersion = true
) )
} }
} }

View file

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ExpirationTimerUtil
import java.io.IOException import java.io.IOException
private val TAG: String = Log.tag(ExpireTimerSettingsRepository::class.java) private val TAG: String = Log.tag(ExpireTimerSettingsRepository::class.java)
@ -38,8 +39,8 @@ class ExpireTimerSettingsRepository(val context: Context) {
consumer.invoke(Result.failure(e)) consumer.invoke(Result.failure(e))
} }
} else { } else {
SignalDatabase.recipients.setExpireMessages(recipientId, newExpirationTime) val expireTimerVersion = ExpirationTimerUtil.setExpirationTimer(recipientId, newExpirationTime)
val outgoingMessage = OutgoingMessage.expirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L) val outgoingMessage = OutgoingMessage.expirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L, expireTimerVersion)
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), MessageSender.SendType.SIGNAL, null, null) MessageSender.send(context, outgoingMessage, getThreadId(recipientId), MessageSender.SendType.SIGNAL, null, null)
consumer.invoke(Result.success(newExpirationTime)) consumer.invoke(Result.success(newExpirationTime))
} }

View file

@ -341,8 +341,9 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
return if (capabilities != null) { return if (capabilities != null) {
TextUtils.concat( TextUtils.concat(
colorize("DeleteSync", capabilities.deleteSync),
", ", ", ",
colorize("DeleteSync", capabilities.deleteSync) colorize("Expire Timer Version", capabilities.versionedExpirationTimer)
) )
} else { } else {
"Recipient not found!" "Recipient not found!"

View file

@ -716,7 +716,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
} }
if (groupState?.disappearingMessagesTimer != null) { if (groupState?.disappearingMessagesTimer != null) {
recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer!!.duration) recipients.setExpireMessagesForGroup(groupRecipientId, groupState.disappearingMessagesTimer!!.duration)
} }
if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) { if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {
@ -843,7 +843,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
} }
if (decryptedGroup.disappearingMessagesTimer != null) { if (decryptedGroup.disappearingMessagesTimer != null) {
recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer!!.duration) recipients.setExpireMessagesForGroup(groupRecipientId, decryptedGroup.disappearingMessagesTimer!!.duration)
} }
if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) { if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {

View file

@ -169,6 +169,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val SMS_SUBSCRIPTION_ID = "subscription_id" const val SMS_SUBSCRIPTION_ID = "subscription_id"
const val EXPIRES_IN = "expires_in" const val EXPIRES_IN = "expires_in"
const val EXPIRE_STARTED = "expire_started" const val EXPIRE_STARTED = "expire_started"
const val EXPIRE_TIMER_VERSION = "expire_timer_version"
const val NOTIFIED = "notified" const val NOTIFIED = "notified"
const val NOTIFIED_TIMESTAMP = "notified_timestamp" const val NOTIFIED_TIMESTAMP = "notified_timestamp"
const val UNIDENTIFIED = "unidentified" const val UNIDENTIFIED = "unidentified"
@ -264,7 +265,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$LATEST_REVISION_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE, $LATEST_REVISION_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
$ORIGINAL_MESSAGE_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE, $ORIGINAL_MESSAGE_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
$REVISION_NUMBER INTEGER DEFAULT 0, $REVISION_NUMBER INTEGER DEFAULT 0,
$MESSAGE_EXTRAS BLOB DEFAULT NULL $MESSAGE_EXTRAS BLOB DEFAULT NULL,
$EXPIRE_TIMER_VERSION INTEGER DEFAULT 1 NOT NULL
) )
""" """
@ -321,6 +323,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
SMS_SUBSCRIPTION_ID, SMS_SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRES_IN,
EXPIRE_STARTED, EXPIRE_STARTED,
EXPIRE_TIMER_VERSION,
NOTIFIED, NOTIFIED,
QUOTE_ID, QUOTE_ID,
QUOTE_AUTHOR, QUOTE_AUTHOR,
@ -2404,6 +2407,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val timestamp = cursor.requireLong(DATE_SENT) val timestamp = cursor.requireLong(DATE_SENT)
val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID) val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
val expiresIn = cursor.requireLong(EXPIRES_IN) val expiresIn = cursor.requireLong(EXPIRES_IN)
val expireTimerVersion = cursor.requireInt(EXPIRE_TIMER_VERSION)
val viewOnce = cursor.requireLong(VIEW_ONCE) == 1L val viewOnce = cursor.requireLong(VIEW_ONCE) == 1L
val threadId = cursor.requireLong(THREAD_ID) val threadId = cursor.requireLong(THREAD_ID)
val threadRecipient = Recipient.resolved(threads.getRecipientIdForThreadId(threadId)!!) val threadRecipient = Recipient.resolved(threads.getRecipientIdForThreadId(threadId)!!)
@ -2480,7 +2484,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
OutgoingMessage.expirationUpdateMessage( OutgoingMessage.expirationUpdateMessage(
threadRecipient = threadRecipient, threadRecipient = threadRecipient,
sentTimeMillis = timestamp, sentTimeMillis = timestamp,
expiresIn = expiresIn expiresIn = expiresIn,
expireTimerVersion = expireTimerVersion
) )
} else if (MessageTypes.isPaymentsNotification(outboxType)) { } else if (MessageTypes.isPaymentsNotification(outboxType)) {
OutgoingMessage.paymentNotificationMessage( OutgoingMessage.paymentNotificationMessage(
@ -2539,6 +2544,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
attachments = attachments, attachments = attachments,
timestamp = timestamp, timestamp = timestamp,
expiresIn = expiresIn, expiresIn = expiresIn,
expireTimerVersion = expireTimerVersion,
viewOnce = viewOnce, viewOnce = viewOnce,
distributionType = distributionType, distributionType = distributionType,
storyType = storyType, storyType = storyType,
@ -2971,6 +2977,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(DATE_RECEIVED, editedMessage?.dateReceived ?: System.currentTimeMillis()) contentValues.put(DATE_RECEIVED, editedMessage?.dateReceived ?: System.currentTimeMillis())
contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId) contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId)
contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn) contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn)
contentValues.put(EXPIRE_TIMER_VERSION, editedMessage?.expireTimerVersion ?: message.expireTimerVersion)
contentValues.put(VIEW_ONCE, message.isViewOnce) contentValues.put(VIEW_ONCE, message.isViewOnce)
contentValues.put(FROM_RECIPIENT_ID, Recipient.self().id.serialize()) contentValues.put(FROM_RECIPIENT_ID, Recipient.self().id.serialize())
contentValues.put(FROM_DEVICE_ID, SignalStore.account.deviceId) contentValues.put(FROM_DEVICE_ID, SignalStore.account.deviceId)
@ -5214,6 +5221,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID) val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
val expiresIn = cursor.requireLong(EXPIRES_IN) val expiresIn = cursor.requireLong(EXPIRES_IN)
val expireStarted = cursor.requireLong(EXPIRE_STARTED) val expireStarted = cursor.requireLong(EXPIRE_STARTED)
val expireTimerVersion = cursor.requireInt(EXPIRE_TIMER_VERSION)
val unidentified = cursor.requireBoolean(UNIDENTIFIED) val unidentified = cursor.requireBoolean(UNIDENTIFIED)
val isViewOnce = cursor.requireBoolean(VIEW_ONCE) val isViewOnce = cursor.requireBoolean(VIEW_ONCE)
val remoteDelete = cursor.requireBoolean(REMOTE_DELETED) val remoteDelete = cursor.requireBoolean(REMOTE_DELETED)
@ -5296,6 +5304,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
subscriptionId, subscriptionId,
expiresIn, expiresIn,
expireStarted, expireStarted,
expireTimerVersion,
isViewOnce, isViewOnce,
hasReadReceipt, hasReadReceipt,
quote, quote,

View file

@ -25,6 +25,7 @@ import org.signal.core.util.orNull
import org.signal.core.util.readToList import org.signal.core.util.readToList
import org.signal.core.util.readToSet import org.signal.core.util.readToSet
import org.signal.core.util.readToSingleBoolean import org.signal.core.util.readToSingleBoolean
import org.signal.core.util.readToSingleInt
import org.signal.core.util.readToSingleLong import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob import org.signal.core.util.requireBlob
@ -166,6 +167,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
const val CALL_VIBRATE = "call_vibrate" const val CALL_VIBRATE = "call_vibrate"
const val MUTE_UNTIL = "mute_until" const val MUTE_UNTIL = "mute_until"
const val MESSAGE_EXPIRATION_TIME = "message_expiration_time" const val MESSAGE_EXPIRATION_TIME = "message_expiration_time"
const val MESSAGE_EXPIRATION_TIME_VERSION = "message_expiration_time_version"
const val SEALED_SENDER_MODE = "sealed_sender_mode" const val SEALED_SENDER_MODE = "sealed_sender_mode"
const val STORAGE_SERVICE_ID = "storage_service_id" const val STORAGE_SERVICE_ID = "storage_service_id"
const val STORAGE_SERVICE_PROTO = "storage_service_proto" const val STORAGE_SERVICE_PROTO = "storage_service_proto"
@ -263,7 +265,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
$NICKNAME_GIVEN_NAME TEXT DEFAULT NULL, $NICKNAME_GIVEN_NAME TEXT DEFAULT NULL,
$NICKNAME_FAMILY_NAME TEXT DEFAULT NULL, $NICKNAME_FAMILY_NAME TEXT DEFAULT NULL,
$NICKNAME_JOINED_NAME TEXT DEFAULT NULL, $NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
$NOTE TEXT DEFAULT NULL $NOTE TEXT DEFAULT NULL,
$MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL
) )
""" """
@ -307,6 +310,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
CALL_VIBRATE, CALL_VIBRATE,
MUTE_UNTIL, MUTE_UNTIL,
MESSAGE_EXPIRATION_TIME, MESSAGE_EXPIRATION_TIME,
MESSAGE_EXPIRATION_TIME_VERSION,
SEALED_SENDER_MODE, SEALED_SENDER_MODE,
STORAGE_SERVICE_ID, STORAGE_SERVICE_ID,
MENTION_SETTING, MENTION_SETTING,
@ -410,6 +414,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long { fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long {
var value: Long = 0 var value: Long = 0
value = Bitmask.update(value, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isDeleteSync).serialize().toLong()) value = Bitmask.update(value, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isDeleteSync).serialize().toLong())
value = Bitmask.update(value, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isVersionedExpirationTimer).serialize().toLong())
return value return value
} }
} }
@ -1473,7 +1478,27 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
StorageSyncHelper.scheduleSyncForDataChange() StorageSyncHelper.scheduleSyncForDataChange()
} }
fun setExpireMessages(id: RecipientId, expiration: Int) { fun setExpireMessagesAndIncrementVersion(id: RecipientId, expiration: Int): Int {
val version = writableDatabase.rawQuery(
"""
UPDATE $TABLE_NAME
SET $MESSAGE_EXPIRATION_TIME = $expiration,
$MESSAGE_EXPIRATION_TIME_VERSION = MIN($MESSAGE_EXPIRATION_TIME_VERSION + 1, ${Int.MAX_VALUE})
WHERE $ID = ${id.serialize()}
RETURNING $MESSAGE_EXPIRATION_TIME_VERSION
""",
null
).readToSingleInt(defaultValue = 1)
AppDependencies.databaseObserver.notifyRecipientChanged(id)
return version
}
/**
* Sets the expiration timer without incrementing the version. Will eventually be removed once everyone has the ability to understand expireTimerVersions.
*/
fun setExpireMessagesWithoutIncrementingVersion(id: RecipientId, expiration: Int) {
val values = ContentValues(1).apply { val values = ContentValues(1).apply {
put(MESSAGE_EXPIRATION_TIME, expiration) put(MESSAGE_EXPIRATION_TIME, expiration)
} }
@ -1482,6 +1507,23 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
} }
/**
* Groups do not have an expireTimerVersion, and therefore we do not need to provide them.
*/
fun setExpireMessagesForGroup(id: RecipientId, expiration: Int) {
setExpireMessages(id, expiration, 1)
}
fun setExpireMessages(id: RecipientId, expiration: Int, expirationVersion: Int) {
val values = contentValuesOf(
MESSAGE_EXPIRATION_TIME to expiration,
MESSAGE_EXPIRATION_TIME_VERSION to expirationVersion
)
if (update(id, values)) {
AppDependencies.databaseObserver.notifyRecipientChanged(id)
}
}
fun setSealedSenderAccessMode(id: RecipientId, sealedSenderAccessMode: SealedSenderAccessMode) { fun setSealedSenderAccessMode(id: RecipientId, sealedSenderAccessMode: SealedSenderAccessMode) {
val values = ContentValues(1).apply { val values = ContentValues(1).apply {
put(SEALED_SENDER_MODE, sealedSenderAccessMode.mode) put(SEALED_SENDER_MODE, sealedSenderAccessMode.mode)
@ -3988,6 +4030,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
AVATAR_COLOR to primaryRecord.avatarColor.serialize(), AVATAR_COLOR to primaryRecord.avatarColor.serialize(),
CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null), CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null),
MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages, MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages,
MESSAGE_EXPIRATION_TIME_VERSION to max(primaryRecord.expireTimerVersion, secondaryRecord.expireTimerVersion),
REGISTERED to RegisteredState.REGISTERED.id, REGISTERED to RegisteredState.REGISTERED.id,
SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName, SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName,
SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName, SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName,
@ -4591,6 +4634,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
// const val PNP = 7 // const val PNP = 7
// const val PAYMENT_ACTIVATION = 8 // const val PAYMENT_ACTIVATION = 8
const val DELETE_SYNC = 9 const val DELETE_SYNC = 9
const val VERSIONED_EXPIRATION_TIMER = 10
// IMPORTANT: We cannot sore more than 32 capabilities in the bitmask. // IMPORTANT: We cannot sore more than 32 capabilities in the bitmask.
} }

View file

@ -135,6 +135,7 @@ object RecipientTableCursorUtil {
messageRingtone = Util.uri(cursor.requireString(RecipientTable.MESSAGE_RINGTONE)), messageRingtone = Util.uri(cursor.requireString(RecipientTable.MESSAGE_RINGTONE)),
callRingtone = Util.uri(cursor.requireString(RecipientTable.CALL_RINGTONE)), callRingtone = Util.uri(cursor.requireString(RecipientTable.CALL_RINGTONE)),
expireMessages = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME), expireMessages = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME),
expireTimerVersion = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION),
registered = RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED)), registered = RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED)),
profileKey = profileKey, profileKey = profileKey,
expiringProfileKeyCredential = expiringProfileKeyCredential, expiringProfileKeyCredential = expiringProfileKeyCredential,
@ -175,7 +176,8 @@ object RecipientTableCursorUtil {
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES) val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
return RecipientRecord.Capabilities( return RecipientRecord.Capabilities(
rawBits = capabilities, rawBits = capabilities,
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt()) deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt()),
versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH).toInt())
) )
} }

View file

@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V237_ResetGroupForc
import org.thoughtcrime.securesms.database.helpers.migration.V238_AddGroupSendEndorsementsColumns import org.thoughtcrime.securesms.database.helpers.migration.V238_AddGroupSendEndorsementsColumns
import org.thoughtcrime.securesms.database.helpers.migration.V239_MessageFullTextSearchEmojiSupport import org.thoughtcrime.securesms.database.helpers.migration.V239_MessageFullTextSearchEmojiSupport
import org.thoughtcrime.securesms.database.helpers.migration.V240_MessageFullTextSearchSecureDelete import org.thoughtcrime.securesms.database.helpers.migration.V240_MessageFullTextSearchSecureDelete
import org.thoughtcrime.securesms.database.helpers.migration.V241_ExpireTimerVersion
/** /**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@ -198,10 +199,11 @@ object SignalDatabaseMigrations {
237 to V237_ResetGroupForceUpdateTimestamps, 237 to V237_ResetGroupForceUpdateTimestamps,
238 to V238_AddGroupSendEndorsementsColumns, 238 to V238_AddGroupSendEndorsementsColumns,
239 to V239_MessageFullTextSearchEmojiSupport, 239 to V239_MessageFullTextSearchEmojiSupport,
240 to V240_MessageFullTextSearchSecureDelete 240 to V240_MessageFullTextSearchSecureDelete,
241 to V241_ExpireTimerVersion
) )
const val DATABASE_VERSION = 240 const val DATABASE_VERSION = 241
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
/**
* Migration for new field tracking expiration timer version.
*/
object V241_ExpireTimerVersion : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE message ADD COLUMN expire_timer_version INTEGER DEFAULT 1 NOT NULL;")
db.execSQL("ALTER TABLE recipient ADD COLUMN message_expiration_time_version INTEGER DEFAULT 1 NOT NULL;")
db.execSQL(
"""
UPDATE recipient
SET message_expiration_time_version = 2
WHERE message_expiration_time > 0
"""
)
}
}

View file

@ -48,6 +48,7 @@ public class InMemoryMessageRecord extends MessageRecord {
-1, -1,
0, 0,
System.currentTimeMillis(), System.currentTimeMillis(),
1,
false, false,
false, false,
Collections.emptyList(), Collections.emptyList(),

View file

@ -99,6 +99,7 @@ public abstract class MessageRecord extends DisplayRecord {
private final int subscriptionId; private final int subscriptionId;
private final long expiresIn; private final long expiresIn;
private final long expireStarted; private final long expireStarted;
private final int expireTimerVersion;
private final boolean unidentified; private final boolean unidentified;
private final List<ReactionRecord> reactions; private final List<ReactionRecord> reactions;
private final long serverTimestamp; private final long serverTimestamp;
@ -119,6 +120,7 @@ public abstract class MessageRecord extends DisplayRecord {
int subscriptionId, int subscriptionId,
long expiresIn, long expiresIn,
long expireStarted, long expireStarted,
int expireTimerVersion,
boolean hasReadReceipt, boolean hasReadReceipt,
boolean unidentified, boolean unidentified,
@NonNull List<ReactionRecord> reactions, @NonNull List<ReactionRecord> reactions,
@ -140,6 +142,7 @@ public abstract class MessageRecord extends DisplayRecord {
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.expireStarted = expireStarted; this.expireStarted = expireStarted;
this.expireTimerVersion = expireTimerVersion;
this.unidentified = unidentified; this.unidentified = unidentified;
this.reactions = reactions; this.reactions = reactions;
this.serverTimestamp = dateServer; this.serverTimestamp = dateServer;
@ -754,6 +757,10 @@ public abstract class MessageRecord extends DisplayRecord {
return expireStarted; return expireStarted;
} }
public int getExpireTimerVersion() {
return expireTimerVersion;
}
public boolean isUnidentified() { public boolean isUnidentified() {
return unidentified; return unidentified;
} }

View file

@ -94,6 +94,7 @@ public class MmsMessageRecord extends MessageRecord {
int subscriptionId, int subscriptionId,
long expiresIn, long expiresIn,
long expireStarted, long expireStarted,
int expireTimerVersion,
boolean viewOnce, boolean viewOnce,
boolean hasReadReceipt, boolean hasReadReceipt,
@Nullable Quote quote, @Nullable Quote quote,
@ -121,7 +122,7 @@ public class MmsMessageRecord extends MessageRecord {
{ {
super(id, body, fromRecipient, fromDeviceId, toRecipient, super(id, body, fromRecipient, fromDeviceId, toRecipient,
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt, dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt,
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, hasReadReceipt, mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, expireTimerVersion, hasReadReceipt,
unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, messageExtras); unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, messageExtras);
this.slideDeck = slideDeck; this.slideDeck = slideDeck;
@ -323,7 +324,7 @@ public class MmsMessageRecord extends MessageRecord {
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) { public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(), return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf, hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
@ -331,7 +332,7 @@ public class MmsMessageRecord extends MessageRecord {
public @NonNull MmsMessageRecord withoutQuote() { public @NonNull MmsMessageRecord withoutQuote() {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(), return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
@ -353,7 +354,7 @@ public class MmsMessageRecord extends MessageRecord {
SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(slideAttachments); SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(slideAttachments);
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), slideDeck, return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), slideDeck,
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
@ -361,7 +362,7 @@ public class MmsMessageRecord extends MessageRecord {
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) { public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(), return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(), getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
@ -370,7 +371,7 @@ public class MmsMessageRecord extends MessageRecord {
public @NonNull MmsMessageRecord withCall(@Nullable CallTable.Call call) { public @NonNull MmsMessageRecord withCall(@Nullable CallTable.Call call) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(), return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(), getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());

View file

@ -43,6 +43,7 @@ data class RecipientRecord(
val messageRingtone: Uri?, val messageRingtone: Uri?,
val callRingtone: Uri?, val callRingtone: Uri?,
val expireMessages: Int, val expireMessages: Int,
val expireTimerVersion: Int,
val registered: RegisteredState, val registered: RegisteredState,
val profileKey: ByteArray?, val profileKey: ByteArray?,
val expiringProfileKeyCredential: ExpiringProfileKeyCredential?, val expiringProfileKeyCredential: ExpiringProfileKeyCredential?,
@ -119,13 +120,15 @@ data class RecipientRecord(
data class Capabilities( data class Capabilities(
val rawBits: Long, val rawBits: Long,
val deleteSync: Recipient.Capability val deleteSync: Recipient.Capability,
val versionedExpirationTimer: Recipient.Capability
) { ) {
companion object { companion object {
@JvmField @JvmField
val UNKNOWN = Capabilities( val UNKNOWN = Capabilities(
0, rawBits = 0,
Recipient.Capability.UNKNOWN deleteSync = Recipient.Capability.UNKNOWN,
versionedExpirationTimer = Recipient.Capability.UNKNOWN
) )
} }
} }

View file

@ -266,6 +266,7 @@ public class IndividualSendJob extends PushSendJob {
.withAttachments(serviceAttachments) .withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
.withExpireTimerVersion(message.getExpireTimerVersion())
.withViewOnce(message.isViewOnce()) .withViewOnce(message.isViewOnce())
.withProfileKey(profileKey.orElse(null)) .withProfileKey(profileKey.orElse(null))
.withSticker(sticker.orElse(null)) .withSticker(sticker.orElse(null))

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.jobs package org.thoughtcrime.securesms.jobs
import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.InvalidMessageException import org.signal.libsignal.protocol.InvalidMessageException
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
@ -92,7 +93,14 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo
} }
if (contact.expirationTimer.isPresent) { if (contact.expirationTimer.isPresent) {
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get()) if (contact.expirationTimerVersion.isPresent && contact.expirationTimerVersion.get() > recipient.expireTimerVersion) {
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get(), contact.expirationTimerVersion.orElse(1))
} else if (contact.expirationTimerVersion.isAbsent()) {
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
recipients.setExpireMessagesWithoutIncrementingVersion(recipient.id, contact.expirationTimer.get())
} else {
Log.w(TAG, "[ContactSync] ${recipient.id} was synced with an old expiration timer. Ignoring. Recieved: ${contact.expirationTimerVersion.get()} Current: ${recipient.expireTimerVersion}")
}
} }
if (contact.profileKey.isPresent) { if (contact.profileKey.isPresent) {

View file

@ -170,6 +170,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()), ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds())
: Optional.empty(), : Optional.empty(),
Optional.of(recipient.getExpireTimerVersion()),
Optional.ofNullable(inboxPositions.get(recipientId)), Optional.ofNullable(inboxPositions.get(recipientId)),
archived.contains(recipientId))); archived.contains(recipientId)));
@ -219,13 +220,14 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Set<RecipientId> archived = SignalDatabase.threads().getArchivedRecipients(); Set<RecipientId> archived = SignalDatabase.threads().getArchivedRecipients();
for (Recipient recipient : recipients) { for (Recipient recipient : recipients) {
Optional<IdentityRecord> identity = AppDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId()); Optional<IdentityRecord> identity = AppDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId());
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity); Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.ofNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context)); Optional<String> name = Optional.ofNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
boolean blocked = recipient.isBlocked(); boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.empty(); Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.empty();
Optional<Integer> inboxPosition = Optional.ofNullable(inboxPositions.get(recipient.getId())); Optional<Integer> expireTimerVersion = Optional.of(recipient.getExpireTimerVersion());
Optional<Integer> inboxPosition = Optional.ofNullable(inboxPositions.get(recipient.getId()));
out.write(new DeviceContact(recipient.getAci(), out.write(new DeviceContact(recipient.getAci(),
recipient.getE164(), recipient.getE164(),
@ -235,6 +237,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
verified, verified,
profileKey, profileKey,
expireTimer, expireTimer,
expireTimerVersion,
inboxPosition, inboxPosition,
archived.contains(recipient.getId()))); archived.contains(recipient.getId())));
} }
@ -252,6 +255,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Optional.empty(), Optional.empty(),
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()), ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
self.getExpiresInSeconds() > 0 ? Optional.of(self.getExpiresInSeconds()) : Optional.empty(), self.getExpiresInSeconds() > 0 ? Optional.of(self.getExpiresInSeconds()) : Optional.empty(),
Optional.of(self.getExpireTimerVersion()),
Optional.ofNullable(inboxPositions.get(self.getId())), Optional.ofNullable(inboxPositions.get(self.getId())),
archived.contains(self.getId()))); archived.contains(self.getId())));
} }

View file

@ -84,6 +84,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
profileKey, profileKey,
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
Optional.empty(),
false)); false));
out.close(); out.close();

View file

@ -221,6 +221,11 @@ public class RefreshOwnProfileJob extends BaseJob {
AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
} }
if (!Recipient.self().getVersionedExpirationTimerCapability().isSupported() && capabilities.isVersionedExpirationTimer()) {
Log.d(TAG, "Transitioned to versioned expiration timer capable, notify linked devices in case we were the last one");
AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
}
SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities); SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities);
} }

View file

@ -158,7 +158,7 @@ object DataMessageProcessor {
when { when {
message.isInvalid -> handleInvalidMessage(context, senderRecipient.id, groupId, envelope.timestamp!!) message.isInvalid -> handleInvalidMessage(context, senderRecipient.id, groupId, envelope.timestamp!!)
message.isEndSession -> insertResult = handleEndSessionMessage(context, senderRecipient.id, envelope, metadata) message.isEndSession -> insertResult = handleEndSessionMessage(context, senderRecipient.id, envelope, metadata)
message.isExpirationUpdate -> insertResult = handleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient.id, groupId, message.expireTimerDuration, receivedTime, false) message.isExpirationUpdate -> insertResult = handleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient.id, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime, false)
message.isStoryReaction -> insertResult = handleStoryReaction(context, envelope, metadata, message, senderRecipient.id, groupId) message.isStoryReaction -> insertResult = handleStoryReaction(context, envelope, metadata, message, senderRecipient.id, groupId)
message.reaction != null -> messageId = handleReaction(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry) message.reaction != null -> messageId = handleReaction(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry)
message.hasRemoteDelete -> messageId = handleRemoteDelete(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry) message.hasRemoteDelete -> messageId = handleRemoteDelete(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry)
@ -321,10 +321,11 @@ object DataMessageProcessor {
private fun handleExpirationUpdate( private fun handleExpirationUpdate(
envelope: Envelope, envelope: Envelope,
metadata: EnvelopeMetadata, metadata: EnvelopeMetadata,
senderRecipientId: RecipientId, senderRecipient: Recipient,
threadRecipientId: RecipientId, threadRecipientId: RecipientId,
groupId: GroupId.V2?, groupId: GroupId.V2?,
expiresIn: Duration, expiresIn: Duration,
expireTimerVersion: Int?,
receivedTime: Long, receivedTime: Long,
sideEffect: Boolean sideEffect: Boolean
): InsertResult? { ): InsertResult? {
@ -340,10 +341,15 @@ object DataMessageProcessor {
return null return null
} }
if (expireTimerVersion != null && expireTimerVersion < senderRecipient.expireTimerVersion) {
log(envelope.timestamp!!, "Old expireTimerVersion. Received: $expireTimerVersion, Current: ${senderRecipient.expireTimerVersion}. Ignoring.")
return null
}
try { try {
val mediaMessage = IncomingMessage( val mediaMessage = IncomingMessage(
type = MessageType.EXPIRATION_UPDATE, type = MessageType.EXPIRATION_UPDATE,
from = senderRecipientId, from = senderRecipient.id,
sentTimeMillis = envelope.timestamp!! - if (sideEffect) 1 else 0, sentTimeMillis = envelope.timestamp!! - if (sideEffect) 1 else 0,
serverTimeMillis = envelope.serverTimestamp!!, serverTimeMillis = envelope.serverTimestamp!!,
receivedTimeMillis = receivedTime, receivedTimeMillis = receivedTime,
@ -353,7 +359,13 @@ object DataMessageProcessor {
) )
val insertResult: InsertResult? = SignalDatabase.messages.insertMessageInbox(mediaMessage, -1).orNull() val insertResult: InsertResult? = SignalDatabase.messages.insertMessageInbox(mediaMessage, -1).orNull()
SignalDatabase.recipients.setExpireMessages(threadRecipientId, expiresIn.inWholeSeconds.toInt())
if (expireTimerVersion != null) {
SignalDatabase.recipients.setExpireMessages(threadRecipientId, expiresIn.inWholeSeconds.toInt(), expireTimerVersion)
} else {
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(threadRecipientId, expiresIn.inWholeSeconds.toInt())
}
if (insertResult != null) { if (insertResult != null) {
return insertResult return insertResult
@ -372,15 +384,16 @@ object DataMessageProcessor {
fun handlePossibleExpirationUpdate( fun handlePossibleExpirationUpdate(
envelope: Envelope, envelope: Envelope,
metadata: EnvelopeMetadata, metadata: EnvelopeMetadata,
senderRecipientId: RecipientId, senderRecipient: Recipient,
threadRecipient: Recipient, threadRecipient: Recipient,
groupId: GroupId.V2?, groupId: GroupId.V2?,
expiresIn: Duration, expiresIn: Duration,
expireTimerVersion: Int?,
receivedTime: Long receivedTime: Long
) { ) {
if (threadRecipient.expiresInSeconds.toLong() != expiresIn.inWholeSeconds) { if (threadRecipient.expiresInSeconds.toLong() != expiresIn.inWholeSeconds || ((expireTimerVersion ?: -1) > threadRecipient.expireTimerVersion)) {
warn(envelope.timestamp!!, "Message expire time didn't match thread expire time. Handling timer update.") warn(envelope.timestamp!!, "Message expire time didn't match thread expire time. Handling timer update.")
handleExpirationUpdate(envelope, metadata, senderRecipientId, threadRecipient.id, groupId, expiresIn, receivedTime, true) handleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient.id, groupId, expiresIn, expireTimerVersion, receivedTime, true)
} }
} }
@ -741,7 +754,7 @@ object DataMessageProcessor {
threadRecipient = senderRecipient threadRecipient = senderRecipient
} }
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime) handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
if (message.hasGroupContext) { if (message.hasGroupContext) {
parentStoryId = GroupReply(storyMessageId.id) parentStoryId = GroupReply(storyMessageId.id)
@ -892,7 +905,7 @@ object DataMessageProcessor {
val attachments: List<Attachment> = message.attachments.toPointersWithinLimit() val attachments: List<Attachment> = message.attachments.toPointersWithinLimit()
val messageRanges: BodyRangeList? = if (message.bodyRanges.isNotEmpty()) message.bodyRanges.asSequence().take(BODY_RANGE_PROCESSING_LIMIT).filter { it.mentionAci == null }.toList().toBodyRangeList() else null val messageRanges: BodyRangeList? = if (message.bodyRanges.isNotEmpty()) message.bodyRanges.asSequence().take(BODY_RANGE_PROCESSING_LIMIT).filter { it.mentionAci == null }.toList().toBodyRangeList() else null
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime) handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
val mediaMessage = IncomingMessage( val mediaMessage = IncomingMessage(
type = MessageType.NORMAL, type = MessageType.NORMAL,
@ -972,7 +985,7 @@ object DataMessageProcessor {
val body = message.body ?: "" val body = message.body ?: ""
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime) handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
notifyTypingStoppedFromIncomingMessage(context, senderRecipient, threadRecipient.id, metadata.sourceDeviceId) notifyTypingStoppedFromIncomingMessage(context, senderRecipient, threadRecipient.id, metadata.sourceDeviceId)

View file

@ -353,6 +353,7 @@ object SyncMessageProcessor {
body = body, body = body,
timestamp = sent.timestamp!!, timestamp = sent.timestamp!!,
expiresIn = targetMessage.expiresIn, expiresIn = targetMessage.expiresIn,
expireTimerVersion = targetMessage.expireTimerVersion,
isSecure = true, isSecure = true,
bodyRanges = bodyRanges, bodyRanges = bodyRanges,
messageToEdit = targetMessage.id messageToEdit = targetMessage.id
@ -366,6 +367,7 @@ object SyncMessageProcessor {
sentTimeMillis = sent.timestamp!!, sentTimeMillis = sent.timestamp!!,
body = body, body = body,
expiresIn = targetMessage.expiresIn, expiresIn = targetMessage.expiresIn,
expireTimerVersion = targetMessage.expireTimerVersion,
isUrgent = true, isUrgent = true,
isSecure = true, isSecure = true,
bodyRanges = bodyRanges, bodyRanges = bodyRanges,
@ -429,6 +431,7 @@ object SyncMessageProcessor {
attachments = syncAttachments.ifEmpty { (targetMessage as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: emptyList() }, attachments = syncAttachments.ifEmpty { (targetMessage as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: emptyList() },
timestamp = sent.timestamp!!, timestamp = sent.timestamp!!,
expiresIn = targetMessage.expiresIn, expiresIn = targetMessage.expiresIn,
expireTimerVersion = targetMessage.expireTimerVersion,
viewOnce = viewOnce, viewOnce = viewOnce,
quote = quote, quote = quote,
contacts = sharedContacts, contacts = sharedContacts,
@ -676,13 +679,26 @@ object SyncMessageProcessor {
} }
val recipient: Recipient = getSyncMessageDestination(sent) val recipient: Recipient = getSyncMessageDestination(sent)
val expirationUpdateMessage: OutgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!, sent.message!!.expireTimerDuration.inWholeMilliseconds)
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null) val expirationUpdateMessage: OutgoingMessage = OutgoingMessage.expirationUpdateMessage(
threadRecipient = recipient,
sentTimeMillis = if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!,
expiresIn = sent.message!!.expireTimerDuration.inWholeMilliseconds,
expireTimerVersion = sent.message!!.expireTimerVersion ?: 1
)
SignalDatabase.messages.markAsSent(messageId, true) if (sent.message?.expireTimerVersion == null) {
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
SignalDatabase.recipients.setExpireMessages(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt()) SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt())
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(messageId, true)
} else if (sent.message!!.expireTimerVersion!! >= recipient.expireTimerVersion) {
SignalDatabase.recipients.setExpireMessages(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt(), sent.message!!.expireTimerVersion!!)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(messageId, true)
} else {
warn(sent.timestamp!!, "[SynchronizeExpiration] Ignoring expire timer update with old version. Received: ${sent.message!!.expireTimerVersion}, Current: ${recipient.expireTimerVersion}")
}
return threadId return threadId
} }
@ -749,7 +765,7 @@ object SyncMessageProcessor {
isSecure = true isSecure = true
) )
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) { if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true) handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
} }
@ -814,7 +830,7 @@ object SyncMessageProcessor {
isSecure = true isSecure = true
) )
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) { if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true) handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
} }
@ -861,7 +877,7 @@ object SyncMessageProcessor {
val expiresInMillis = dataMessage.expireTimerDuration.inWholeMilliseconds val expiresInMillis = dataMessage.expireTimerDuration.inWholeMilliseconds
val bodyRanges = dataMessage.bodyRanges.filter { it.mentionAci == null }.toBodyRangeList() val bodyRanges = dataMessage.bodyRanges.filter { it.mentionAci == null }.toBodyRangeList()
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) { if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true) handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
} }

View file

@ -151,9 +151,10 @@ public class ApplicationMigrations {
static final int CONTACT_LINK_REBUILD = 106; static final int CONTACT_LINK_REBUILD = 106;
static final int DELETE_SYNC_CAPABILITY = 107; static final int DELETE_SYNC_CAPABILITY = 107;
static final int REBUILD_MESSAGE_FTS_INDEX_5 = 108; static final int REBUILD_MESSAGE_FTS_INDEX_5 = 108;
static final int EXPIRE_TIMER_CAPABILITY = 109;
} }
public static final int CURRENT_VERSION = 108; public static final int CURRENT_VERSION = 109;
/** /**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@ -688,6 +689,10 @@ public class ApplicationMigrations {
jobs.put(Version.REBUILD_MESSAGE_FTS_INDEX_5, new RebuildMessageSearchIndexMigrationJob()); jobs.put(Version.REBUILD_MESSAGE_FTS_INDEX_5, new RebuildMessageSearchIndexMigrationJob());
} }
if (lastSeenVersion < Version.EXPIRE_TIMER_CAPABILITY) {
jobs.put(Version.EXPIRE_TIMER_CAPABILITY, new AttributesMigrationJob());
}
return jobs; return jobs;
} }

View file

@ -25,6 +25,7 @@ data class OutgoingMessage(
val body: String = "", val body: String = "",
val distributionType: Int = ThreadTable.DistributionTypes.DEFAULT, val distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
val expiresIn: Long = 0L, val expiresIn: Long = 0L,
val expireTimerVersion: Int = threadRecipient.expireTimerVersion,
val isViewOnce: Boolean = false, val isViewOnce: Boolean = false,
val outgoingQuote: QuoteModel? = null, val outgoingQuote: QuoteModel? = null,
val storyType: StoryType = StoryType.NONE, val storyType: StoryType = StoryType.NONE,
@ -70,6 +71,7 @@ data class OutgoingMessage(
attachments: List<Attachment> = emptyList(), attachments: List<Attachment> = emptyList(),
timestamp: Long, timestamp: Long,
expiresIn: Long = 0L, expiresIn: Long = 0L,
expireTimerVersion: Int = 1,
viewOnce: Boolean = false, viewOnce: Boolean = false,
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT, distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
storyType: StoryType = StoryType.NONE, storyType: StoryType = StoryType.NONE,
@ -92,6 +94,7 @@ data class OutgoingMessage(
attachments = attachments, attachments = attachments,
sentTimeMillis = timestamp, sentTimeMillis = timestamp,
expiresIn = expiresIn, expiresIn = expiresIn,
expireTimerVersion = expireTimerVersion,
isViewOnce = viewOnce, isViewOnce = viewOnce,
distributionType = distributionType, distributionType = distributionType,
storyType = storyType, storyType = storyType,
@ -119,6 +122,7 @@ data class OutgoingMessage(
body: String? = "", body: String? = "",
timestamp: Long, timestamp: Long,
expiresIn: Long = 0L, expiresIn: Long = 0L,
expiresTimerVersion: Int = 1,
viewOnce: Boolean = false, viewOnce: Boolean = false,
storyType: StoryType = StoryType.NONE, storyType: StoryType = StoryType.NONE,
linkPreviews: List<LinkPreview> = emptyList(), linkPreviews: List<LinkPreview> = emptyList(),
@ -132,6 +136,7 @@ data class OutgoingMessage(
attachments = slideDeck.asAttachments(), attachments = slideDeck.asAttachments(),
sentTimeMillis = timestamp, sentTimeMillis = timestamp,
expiresIn = expiresIn, expiresIn = expiresIn,
expireTimerVersion = expiresTimerVersion,
isViewOnce = viewOnce, isViewOnce = viewOnce,
storyType = storyType, storyType = storyType,
linkPreviews = linkPreviews, linkPreviews = linkPreviews,
@ -143,8 +148,8 @@ data class OutgoingMessage(
val subscriptionId = -1 val subscriptionId = -1
fun withExpiry(expiresIn: Long): OutgoingMessage { fun withExpiry(expiresIn: Long, expireTimerVersion: Int): OutgoingMessage {
return copy(expiresIn = expiresIn) return copy(expiresIn = expiresIn, expireTimerVersion = expireTimerVersion)
} }
fun stripAttachments(): OutgoingMessage { fun stripAttachments(): OutgoingMessage {
@ -351,12 +356,13 @@ data class OutgoingMessage(
* Helper for creating expiration update messages. * Helper for creating expiration update messages.
*/ */
@JvmStatic @JvmStatic
fun expirationUpdateMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage { fun expirationUpdateMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long, expireTimerVersion: Int): OutgoingMessage {
return OutgoingMessage( return OutgoingMessage(
threadRecipient = threadRecipient, threadRecipient = threadRecipient,
sentTimeMillis = sentTimeMillis, sentTimeMillis = sentTimeMillis,
expiresIn = expiresIn, expiresIn = expiresIn,
isExpirationUpdate = true, isExpirationUpdate = true,
expireTimerVersion = expireTimerVersion,
isUrgent = false, isUrgent = false,
isSecure = true isSecure = true
) )

View file

@ -75,9 +75,10 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
long threadId; long threadId;
Recipient recipient = Recipient.resolved(recipientId); Recipient recipient = Recipient.resolved(recipientId);
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds()); long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
ParentStoryId parentStoryId = groupStoryId != Long.MIN_VALUE ? ParentStoryId.deserialize(groupStoryId) : null; int expireTimerVersion = recipient.getExpireTimerVersion();
ParentStoryId parentStoryId = groupStoryId != Long.MIN_VALUE ? ParentStoryId.deserialize(groupStoryId) : null;
switch (replyMethod) { switch (replyMethod) {
case GroupMessage: { case GroupMessage: {
@ -86,6 +87,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
new LinkedList<>(), new LinkedList<>(),
System.currentTimeMillis(), System.currentTimeMillis(),
expiresIn, expiresIn,
expireTimerVersion,
false, false,
0, 0,
StoryType.NONE, StoryType.NONE,

View file

@ -84,6 +84,7 @@ class Recipient(
private val messageRingtoneUri: Uri? = null, private val messageRingtoneUri: Uri? = null,
private val callRingtoneUri: Uri? = null, private val callRingtoneUri: Uri? = null,
val expiresInSeconds: Int = 0, val expiresInSeconds: Int = 0,
val expireTimerVersion: Int = 1,
private val registeredValue: RegisteredState = RegisteredState.UNKNOWN, private val registeredValue: RegisteredState = RegisteredState.UNKNOWN,
val profileKey: ByteArray? = null, val profileKey: ByteArray? = null,
val expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null, val expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null,
@ -314,9 +315,12 @@ class Recipient(
/** The notification channel, if both set and supported by the system. Otherwise null. */ /** The notification channel, if both set and supported by the system. Otherwise null. */
val notificationChannel: String? = if (!NotificationChannels.supported()) null else notificationChannelValue val notificationChannel: String? = if (!NotificationChannels.supported()) null else notificationChannelValue
/** The user's payment capability. */ /** The user's capability to handle synchronizing deletes across linked devices. */
val deleteSyncCapability: Capability = capabilities.deleteSync val deleteSyncCapability: Capability = capabilities.deleteSync
/** The user's capability to handle tracking an expire timer version. */
val versionedExpirationTimerCapability: Capability = capabilities.versionedExpirationTimer
/** The state around whether we can send sealed sender to this user. */ /** The state around whether we can send sealed sender to this user. */
val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) { val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) {
SealedSenderAccessMode.DISABLED SealedSenderAccessMode.DISABLED

View file

@ -167,6 +167,7 @@ object RecipientCreator {
callVibrate = record.callVibrateState, callVibrate = record.callVibrateState,
isBlocked = record.isBlocked, isBlocked = record.isBlocked,
expiresInSeconds = record.expireMessages, expiresInSeconds = record.expireMessages,
expireTimerVersion = record.expireTimerVersion,
participantIdsValue = participantIds ?: LinkedList(), participantIdsValue = participantIds ?: LinkedList(),
isActiveGroup = groupRecord.map { it.isActive }.orElse(false), isActiveGroup = groupRecord.map { it.isActive }.orElse(false),
profileName = record.signalProfileName, profileName = record.signalProfileName,

View file

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.ExpirationTimerUtil;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
@ -324,21 +325,23 @@ public class RecipientUtil {
/** /**
* Checks if a universal timer is set and if the thread should have it set on it. Attempts to abort quickly and perform * Checks if a universal timer is set and if the thread should have it set on it. Attempts to abort quickly and perform
* minimal database access. * minimal database access.
*
* @return The new expire timer version if the timer was set, otherwise null.
*/ */
@WorkerThread @WorkerThread
public static boolean setAndSendUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, long threadId) { public static @Nullable Integer setAndSendUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
int defaultTimer = SignalStore.settings().getUniversalExpireTimer(); int defaultTimer = SignalStore.settings().getUniversalExpireTimer();
if (defaultTimer == 0 || recipient.isGroup() || recipient.isDistributionList() || recipient.getExpiresInSeconds() != 0 || !recipient.isRegistered()) { if (defaultTimer == 0 || recipient.isGroup() || recipient.isDistributionList() || recipient.getExpiresInSeconds() != 0 || !recipient.isRegistered()) {
return false; return null;
} }
if (threadId == -1 || SignalDatabase.messages().canSetUniversalTimer(threadId)) { if (threadId == -1 || SignalDatabase.messages().canSetUniversalTimer(threadId)) {
SignalDatabase.recipients().setExpireMessages(recipient.getId(), defaultTimer); int expireTimerVersion = ExpirationTimerUtil.setExpirationTimer(recipient.getId(), defaultTimer);
OutgoingMessage outgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, System.currentTimeMillis(), defaultTimer * 1000L); OutgoingMessage outgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, System.currentTimeMillis(), defaultTimer * 1000L, expireTimerVersion);
MessageSender.send(context, outgoingMessage, SignalDatabase.threads().getOrCreateThreadIdFor(recipient), MessageSender.SendType.SIGNAL, null, null); MessageSender.send(context, outgoingMessage, SignalDatabase.threads().getOrCreateThreadIdFor(recipient), MessageSender.SendType.SIGNAL, null, null);
return true; return expireTimerVersion;
} }
return false; return null;
} }
@WorkerThread @WorkerThread

View file

@ -105,12 +105,13 @@ public final class MultiShareSender {
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) { for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId()); Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId());
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions()); List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE; MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE;
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds()); long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
List<Contact> contacts = multiShareArgs.getSharedContacts(); int expireTimerVersion = recipient.getExpireTimerVersion();
SlideDeck slideDeck = new SlideDeck(primarySlideDeck); List<Contact> contacts = multiShareArgs.getSharedContacts();
SlideDeck slideDeck = new SlideDeck(primarySlideDeck);
boolean needsSplit = message != null && boolean needsSplit = message != null &&
message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize; message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
@ -138,6 +139,7 @@ public final class MultiShareSender {
sendType, sendType,
threadId, threadId,
expiresIn, expiresIn,
expireTimerVersion,
multiShareArgs.isViewOnce(), multiShareArgs.isViewOnce(),
mentions, mentions,
recipientSearchKey.isStory(), recipientSearchKey.isStory(),
@ -182,6 +184,7 @@ public final class MultiShareSender {
@NonNull MessageSendType sendType, @NonNull MessageSendType sendType,
long threadId, long threadId,
long expiresIn, long expiresIn,
int expireTimerVersion,
boolean isViewOnce, boolean isViewOnce,
@NonNull List<Mention> validatedMentions, @NonNull List<Mention> validatedMentions,
boolean isStory, boolean isStory,
@ -221,6 +224,7 @@ public final class MultiShareSender {
body, body,
sentTimestamps.getMillis(0), sentTimestamps.getMillis(0),
0L, 0L,
1,
false, false,
storyType.toTextStoryType(), storyType.toTextStoryType(),
buildLinkPreviews(context, multiShareArgs.getLinkPreview()), buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
@ -260,6 +264,7 @@ public final class MultiShareSender {
body, body,
sentTimestamps.getMillis(i), sentTimestamps.getMillis(i),
0L, 0L,
1,
false, false,
storyType, storyType,
Collections.emptyList(), Collections.emptyList(),
@ -277,6 +282,7 @@ public final class MultiShareSender {
body, body,
sentTimestamps.getMillis(0), sentTimestamps.getMillis(0),
expiresIn, expiresIn,
expireTimerVersion,
isViewOnce, isViewOnce,
StoryType.NONE, StoryType.NONE,
buildLinkPreviews(context, multiShareArgs.getLinkPreview()), buildLinkPreviews(context, multiShareArgs.getLinkPreview()),

View file

@ -513,8 +513,12 @@ public class MessageSender {
} }
private static @NonNull OutgoingMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMessage outgoingMessage, long threadId) { private static @NonNull OutgoingMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMessage outgoingMessage, long threadId) {
if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) { if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0) {
return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer())); Integer expireTimerVersion = RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId);
if (expireTimerVersion != null) {
return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()), expireTimerVersion);
}
} }
return outgoingMessage; return outgoingMessage;
} }

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* This exists as a temporary shim to improve the callsites where we'll be setting the expiration timer.
*
* Until the versions that don't understand expiration timers expire, we'll have to check capabilities before incrementing the version.
*
* After those old clients expire, we can remove this shim entirely and call the RecipientTable methods directly.
*/
object ExpirationTimerUtil {
@JvmStatic
fun setExpirationTimer(recipientId: RecipientId, expirationTimeSeconds: Int): Int {
val selfCapable = Recipient.self().versionedExpirationTimerCapability == Recipient.Capability.SUPPORTED
val recipientCapable = Recipient.resolved(recipientId).let { it.versionedExpirationTimerCapability == Recipient.Capability.SUPPORTED || it.expireTimerVersion > 2 }
return if (selfCapable && recipientCapable) {
SignalDatabase.recipients.setExpireMessagesAndIncrementVersion(recipientId, expirationTimeSeconds)
} else {
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(recipientId, expirationTimeSeconds)
1
}
}
}

View file

@ -44,6 +44,7 @@ object RecipientDatabaseTestUtils {
messageRingtone: Uri = Uri.EMPTY, messageRingtone: Uri = Uri.EMPTY,
callRingtone: Uri = Uri.EMPTY, callRingtone: Uri = Uri.EMPTY,
expireMessages: Int = 0, expireMessages: Int = 0,
expireTimerVersion: Int = 1,
registered: RecipientTable.RegisteredState = RecipientTable.RegisteredState.REGISTERED, registered: RecipientTable.RegisteredState = RecipientTable.RegisteredState.REGISTERED,
profileKey: ByteArray = Random.nextBytes(32), profileKey: ByteArray = Random.nextBytes(32),
expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null, expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null,
@ -107,6 +108,7 @@ object RecipientDatabaseTestUtils {
messageRingtone = messageRingtone, messageRingtone = messageRingtone,
callRingtone = callRingtone, callRingtone = callRingtone,
expireMessages = expireMessages, expireMessages = expireMessages,
expireTimerVersion = expireTimerVersion,
registered = registered, registered = registered,
profileKey = profileKey, profileKey = profileKey,
expiringProfileKeyCredential = expiringProfileKeyCredential, expiringProfileKeyCredential = expiringProfileKeyCredential,
@ -124,7 +126,8 @@ object RecipientDatabaseTestUtils {
sealedSenderAccessMode = sealedSenderAccessMode, sealedSenderAccessMode = sealedSenderAccessMode,
capabilities = RecipientRecord.Capabilities( capabilities = RecipientRecord.Capabilities(
rawBits = capabilities, rawBits = capabilities,
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()) deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()),
versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.VERSIONED_EXPIRATION_TIMER, RecipientTable.Capabilities.BIT_LENGTH).toInt())
), ),
storageId = storageId, storageId = storageId,
mentionSetting = mentionSetting, mentionSetting = mentionSetting,

View file

@ -20,6 +20,7 @@ object TestMms {
sentTimeMillis: Long = System.currentTimeMillis(), sentTimeMillis: Long = System.currentTimeMillis(),
receivedTimestampMillis: Long = System.currentTimeMillis(), receivedTimestampMillis: Long = System.currentTimeMillis(),
expiresIn: Long = 0, expiresIn: Long = 0,
expireTimerVersion: Int = 1,
viewOnce: Boolean = false, viewOnce: Boolean = false,
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT, distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
type: Long = MessageTypes.BASE_INBOX_TYPE, type: Long = MessageTypes.BASE_INBOX_TYPE,
@ -29,23 +30,24 @@ object TestMms {
storyType: StoryType = StoryType.NONE storyType: StoryType = StoryType.NONE
): Long { ): Long {
val message = OutgoingMessage( val message = OutgoingMessage(
recipient, recipient = recipient,
body, body = body,
emptyList(), attachments = emptyList(),
sentTimeMillis, timestamp = sentTimeMillis,
expiresIn, expiresIn = expiresIn,
viewOnce, expireTimerVersion = expireTimerVersion,
distributionType, viewOnce = viewOnce,
storyType, distributionType = distributionType,
null, storyType = storyType,
false, parentStoryId = null,
null, isStoryReaction = false,
emptyList(), quote = null,
emptyList(), contacts = emptyList(),
emptyList(), previews = emptyList(),
emptySet(), mentions = emptyList(),
emptySet(), networkFailures = emptySet(),
null mismatches = emptySet(),
giftBadge = null
) )
return insert( return insert(
@ -61,7 +63,7 @@ object TestMms {
) )
} }
fun insert( private fun insert(
db: SQLiteDatabase, db: SQLiteDatabase,
message: OutgoingMessage, message: OutgoingMessage,
recipientId: RecipientId = message.threadRecipient.id, recipientId: RecipientId = message.threadRecipient.id,
@ -81,6 +83,7 @@ object TestMms {
put(MessageTable.DATE_RECEIVED, receivedTimestampMillis) put(MessageTable.DATE_RECEIVED, receivedTimestampMillis)
put(MessageTable.SMS_SUBSCRIPTION_ID, message.subscriptionId) put(MessageTable.SMS_SUBSCRIPTION_ID, message.subscriptionId)
put(MessageTable.EXPIRES_IN, message.expiresIn) put(MessageTable.EXPIRES_IN, message.expiresIn)
put(MessageTable.EXPIRE_TIMER_VERSION, message.expireTimerVersion)
put(MessageTable.VIEW_ONCE, message.isViewOnce) put(MessageTable.VIEW_ONCE, message.isViewOnce)
put(MessageTable.FROM_RECIPIENT_ID, recipientId.serialize()) put(MessageTable.FROM_RECIPIENT_ID, recipientId.serialize())
put(MessageTable.TO_RECIPIENT_ID, recipientId.serialize()) put(MessageTable.TO_RECIPIENT_ID, recipientId.serialize())

View file

@ -141,6 +141,7 @@ object FakeMessageRecords {
subscriptionId: Int = -1, subscriptionId: Int = -1,
expiresIn: Long = -1, expiresIn: Long = -1,
expireStarted: Long = -1, expireStarted: Long = -1,
expireTimerVersion: Int = individualRecipient.expireTimerVersion,
viewOnce: Boolean = false, viewOnce: Boolean = false,
hasReadReceipt: Boolean = false, hasReadReceipt: Boolean = false,
quote: Quote? = null, quote: Quote? = null,
@ -178,6 +179,7 @@ object FakeMessageRecords {
subscriptionId, subscriptionId,
expiresIn, expiresIn,
expireStarted, expireStarted,
expireTimerVersion,
viewOnce, viewOnce,
hasReadReceipt, hasReadReceipt,
quote, quote,

View file

@ -1059,6 +1059,7 @@ public class SignalServiceMessageSender {
if (message.getExpiresInSeconds() > 0) { if (message.getExpiresInSeconds() > 0) {
builder.expireTimer(message.getExpiresInSeconds()); builder.expireTimer(message.getExpiresInSeconds());
} }
builder.expireTimerVersion(message.getExpireTimerVersion());
if (message.getProfileKey().isPresent()) { if (message.getProfileKey().isPresent()) {
builder.profileKey(ByteString.of(message.getProfileKey().get())); builder.profileKey(ByteString.of(message.getProfileKey().get()));

View file

@ -55,6 +55,7 @@ class AccountAttributes @JsonCreator constructor(
data class Capabilities @JsonCreator constructor( data class Capabilities @JsonCreator constructor(
@JsonProperty val storage: Boolean, @JsonProperty val storage: Boolean,
@JsonProperty val deleteSync: Boolean @JsonProperty val deleteSync: Boolean,
@JsonProperty val expireTimerVersion: Boolean
) )
} }

View file

@ -34,6 +34,7 @@ class SignalServiceDataMessage private constructor(
val body: Optional<String>, val body: Optional<String>,
val isEndSession: Boolean, val isEndSession: Boolean,
val expiresInSeconds: Int, val expiresInSeconds: Int,
val expireTimerVersion: Int,
val isExpirationUpdate: Boolean, val isExpirationUpdate: Boolean,
val profileKey: Optional<ByteArray>, val profileKey: Optional<ByteArray>,
val isProfileKeyUpdate: Boolean, val isProfileKeyUpdate: Boolean,
@ -79,6 +80,7 @@ class SignalServiceDataMessage private constructor(
private var body: String? = null private var body: String? = null
private var endSession: Boolean = false private var endSession: Boolean = false
private var expiresInSeconds: Int = 0 private var expiresInSeconds: Int = 0
private var expireTimerVersion: Int = 1
private var expirationUpdate: Boolean = false private var expirationUpdate: Boolean = false
private var profileKey: ByteArray? = null private var profileKey: ByteArray? = null
private var profileKeyUpdate: Boolean = false private var profileKeyUpdate: Boolean = false
@ -133,6 +135,11 @@ class SignalServiceDataMessage private constructor(
return this return this
} }
fun withExpireTimerVersion(expireTimerVersion: Int): Builder {
this.expireTimerVersion = expireTimerVersion
return this
}
fun withProfileKey(profileKey: ByteArray?): Builder { fun withProfileKey(profileKey: ByteArray?): Builder {
this.profileKey = profileKey this.profileKey = profileKey
return this return this
@ -225,6 +232,7 @@ class SignalServiceDataMessage private constructor(
body = body.emptyIfStringEmpty(), body = body.emptyIfStringEmpty(),
isEndSession = endSession, isEndSession = endSession,
expiresInSeconds = expiresInSeconds, expiresInSeconds = expiresInSeconds,
expireTimerVersion = expireTimerVersion,
isExpirationUpdate = expirationUpdate, isExpirationUpdate = expirationUpdate,
profileKey = profileKey.asOptional(), profileKey = profileKey.asOptional(),
isProfileKeyUpdate = profileKeyUpdate, isProfileKeyUpdate = profileKeyUpdate,

View file

@ -21,6 +21,7 @@ public class DeviceContact {
private final Optional<VerifiedMessage> verified; private final Optional<VerifiedMessage> verified;
private final Optional<ProfileKey> profileKey; private final Optional<ProfileKey> profileKey;
private final Optional<Integer> expirationTimer; private final Optional<Integer> expirationTimer;
private final Optional<Integer> expirationTimerVersion;
private final Optional<Integer> inboxPosition; private final Optional<Integer> inboxPosition;
private final boolean archived; private final boolean archived;
@ -32,6 +33,7 @@ public class DeviceContact {
Optional<VerifiedMessage> verified, Optional<VerifiedMessage> verified,
Optional<ProfileKey> profileKey, Optional<ProfileKey> profileKey,
Optional<Integer> expirationTimer, Optional<Integer> expirationTimer,
Optional<Integer> expirationTimerVersion,
Optional<Integer> inboxPosition, Optional<Integer> inboxPosition,
boolean archived) boolean archived)
{ {
@ -39,16 +41,17 @@ public class DeviceContact {
throw new IllegalArgumentException("Must have either ACI or E164"); throw new IllegalArgumentException("Must have either ACI or E164");
} }
this.aci = aci; this.aci = aci;
this.e164 = e164; this.e164 = e164;
this.name = name; this.name = name;
this.avatar = avatar; this.avatar = avatar;
this.color = color; this.color = color;
this.verified = verified; this.verified = verified;
this.profileKey = profileKey; this.profileKey = profileKey;
this.expirationTimer = expirationTimer; this.expirationTimer = expirationTimer;
this.inboxPosition = inboxPosition; this.expirationTimerVersion = expirationTimerVersion;
this.archived = archived; this.inboxPosition = inboxPosition;
this.archived = archived;
} }
public Optional<DeviceContactAvatar> getAvatar() { public Optional<DeviceContactAvatar> getAvatar() {
@ -83,6 +86,10 @@ public class DeviceContact {
return expirationTimer; return expirationTimer;
} }
public Optional<Integer> getExpirationTimerVersion() {
return expirationTimerVersion;
}
public Optional<Integer> getInboxPosition() { public Optional<Integer> getInboxPosition() {
return inboxPosition; return inboxPosition;
} }

View file

@ -47,16 +47,17 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
throw new IOException("Missing contact address!"); throw new IOException("Missing contact address!");
} }
Optional<ACI> aci = Optional.ofNullable(ACI.parseOrNull(details.aci)); Optional<ACI> aci = Optional.ofNullable(ACI.parseOrNull(details.aci));
Optional<String> e164 = Optional.ofNullable(details.number); Optional<String> e164 = Optional.ofNullable(details.number);
Optional<String> name = Optional.ofNullable(details.name); Optional<String> name = Optional.ofNullable(details.name);
Optional<DeviceContactAvatar> avatar = Optional.empty(); Optional<DeviceContactAvatar> avatar = Optional.empty();
Optional<String> color = details.color != null ? Optional.of(details.color) : Optional.empty(); Optional<String> color = details.color != null ? Optional.of(details.color) : Optional.empty();
Optional<VerifiedMessage> verified = Optional.empty(); Optional<VerifiedMessage> verified = Optional.empty();
Optional<ProfileKey> profileKey = Optional.empty(); Optional<ProfileKey> profileKey = Optional.empty();
Optional<Integer> expireTimer = Optional.empty(); Optional<Integer> expireTimer = Optional.empty();
Optional<Integer> inboxPosition = Optional.empty(); Optional<Integer> expireTimerVersion = Optional.empty();
boolean archived = false; Optional<Integer> inboxPosition = Optional.empty();
boolean archived = false;
if (details.avatar != null && details.avatar.length != null) { if (details.avatar != null && details.avatar.length != null) {
long avatarLength = details.avatar.length; long avatarLength = details.avatar.length;
@ -103,13 +104,17 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
expireTimer = Optional.of(details.expireTimer); expireTimer = Optional.of(details.expireTimer);
} }
if (details.expireTimerVersion != null && details.expireTimerVersion > 0) {
expireTimerVersion = Optional.of(details.expireTimerVersion);
}
if (details.inboxPosition != null) { if (details.inboxPosition != null) {
inboxPosition = Optional.of(details.inboxPosition); inboxPosition = Optional.of(details.inboxPosition);
} }
archived = details.archived; archived = details.archived;
return new DeviceContact(aci, e164, name, avatar, color, verified, profileKey, expireTimer, inboxPosition, archived); return new DeviceContact(aci, e164, name, avatar, color, verified, profileKey, expireTimer, expireTimerVersion, inboxPosition, archived);
} }
} }

View file

@ -195,12 +195,16 @@ public class SignalServiceProfile {
@JsonProperty @JsonProperty
private boolean deleteSync; private boolean deleteSync;
@JsonProperty
private boolean versionedExpirationTimer;
@JsonCreator @JsonCreator
public Capabilities() {} public Capabilities() {}
public Capabilities(boolean storage, boolean deleteSync) { public Capabilities(boolean storage, boolean deleteSync, boolean versionedExpirationTimer) {
this.storage = storage; this.storage = storage;
this.deleteSync = deleteSync; this.deleteSync = deleteSync;
this.versionedExpirationTimer = versionedExpirationTimer;
} }
public boolean isStorage() { public boolean isStorage() {
@ -210,6 +214,10 @@ public class SignalServiceProfile {
public boolean isDeleteSync() { public boolean isDeleteSync() {
return deleteSync; return deleteSync;
} }
public boolean isVersionedExpirationTimer() {
return versionedExpirationTimer;
}
} }
public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() {

View file

@ -332,6 +332,7 @@ message DataMessage {
optional GroupContextV2 groupV2 = 15; optional GroupContextV2 groupV2 = 15;
optional uint32 flags = 4; optional uint32 flags = 4;
optional uint32 expireTimer = 5; optional uint32 expireTimer = 5;
optional uint32 expireTimerVersion = 23;
optional bytes profileKey = 6; optional bytes profileKey = 6;
optional uint64 timestamp = 7; optional uint64 timestamp = 7;
optional Quote quote = 8; optional Quote quote = 8;
@ -793,17 +794,18 @@ message ContactDetails {
optional uint32 length = 2; optional uint32 length = 2;
} }
optional string number = 1; optional string number = 1;
optional string aci = 9; optional string aci = 9;
optional string name = 2; optional string name = 2;
optional Avatar avatar = 3; optional Avatar avatar = 3;
optional string color = 4; optional string color = 4;
optional Verified verified = 5; optional Verified verified = 5;
optional bytes profileKey = 6; optional bytes profileKey = 6;
reserved /*blocked*/ 7; reserved /*blocked*/ 7;
optional uint32 expireTimer = 8; optional uint32 expireTimer = 8;
optional uint32 inboxPosition = 10; optional uint32 expireTimerVersion = 12;
optional bool archived = 11; optional uint32 inboxPosition = 10;
optional bool archived = 11;
} }
message GroupDetails { message GroupDetails {
@ -817,17 +819,17 @@ message GroupDetails {
optional string e164 = 2; optional string e164 = 2;
} }
optional bytes id = 1; optional bytes id = 1;
optional string name = 2; optional string name = 2;
repeated string membersE164 = 3; repeated string membersE164 = 3;
repeated Member members = 9; repeated Member members = 9;
optional Avatar avatar = 4; optional Avatar avatar = 4;
optional bool active = 5 [default = true]; optional bool active = 5 [default = true];
optional uint32 expireTimer = 6; optional uint32 expireTimer = 6;
optional string color = 7; optional string color = 7;
optional bool blocked = 8; optional bool blocked = 8;
optional uint32 inboxPosition = 10; optional uint32 inboxPosition = 10;
optional bool archived = 11; optional bool archived = 11;
} }
message PaymentAddress { message PaymentAddress {

View file

@ -40,6 +40,7 @@ public class DeviceContactsInputStreamTest {
Optional.of(generateProfileKey()), Optional.of(generateProfileKey()),
Optional.of(0), Optional.of(0),
Optional.of(0), Optional.of(0),
Optional.of(0),
false false
); );
@ -53,6 +54,7 @@ public class DeviceContactsInputStreamTest {
Optional.of(generateProfileKey()), Optional.of(generateProfileKey()),
Optional.of(0), Optional.of(0),
Optional.of(0), Optional.of(0),
Optional.of(0),
false false
); );