mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add call link support to storage service.
This commit is contained in:
committed by
Cody Henthorne
parent
1f2b5e90a3
commit
e247d311d8
@@ -86,7 +86,8 @@ class CallLinkTableTest {
|
||||
linkKeyBytes = roomId,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId? {
|
||||
name = callLink.name,
|
||||
restrictions = callLink.restrictions.toLocal(),
|
||||
expiration = Instant.ofEpochMilli(callLink.expirationMs)
|
||||
)
|
||||
),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ private fun SignalCallRowPreview() {
|
||||
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
|
||||
expiration = Instant.MAX,
|
||||
revoked = false
|
||||
)
|
||||
),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
}
|
||||
Previews.Preview {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
|
||||
/**
|
||||
* Repository for performing update operations on call links:
|
||||
@@ -65,6 +66,7 @@ class UpdateCallLinkRepository(
|
||||
is UpdateCallLinkResult.Delete -> {
|
||||
SignalDatabase.callLinks.markRevoked(credentials.roomId)
|
||||
AppDependencies.jobManager.add(CallLinkUpdateSendJob(credentials.roomId))
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
|
||||
/**
|
||||
@@ -40,22 +41,25 @@ class CreateCallLinkRepository(
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
state = it.state
|
||||
roomId = it.credentials.roomId,
|
||||
credentials = it.credentials,
|
||||
state = it.state,
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
)
|
||||
|
||||
AppDependencies.jobManager.add(
|
||||
CallLinkUpdateSendJob(
|
||||
credentials.roomId,
|
||||
it.credentials.roomId,
|
||||
SyncMessage.CallLinkUpdate.Type.UPDATE
|
||||
)
|
||||
)
|
||||
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
|
||||
EnsureCallLinkCreatedResult.Success(
|
||||
Recipient.resolved(
|
||||
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId).get()
|
||||
SignalDatabase.recipients.getByCallLinkRoomId(it.credentials.roomId).get()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ class CreateCallLinkViewModel(
|
||||
restrictions = Restrictions.ADMIN_APPROVAL,
|
||||
revoked = false,
|
||||
expiration = Instant.MAX
|
||||
)
|
||||
),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
lifecycleDisposable += viewModel.delete().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is UpdateCallLinkResult.Update -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||
is UpdateCallLinkResult.Delete -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||
else -> {
|
||||
Log.w(TAG, "Failed to revoke. $it")
|
||||
toastFailure()
|
||||
@@ -213,7 +213,8 @@ private fun CallLinkDetailsPreview() {
|
||||
revoked = false,
|
||||
restrictions = Restrictions.NONE,
|
||||
expiration = Instant.MAX
|
||||
)
|
||||
),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ class CallLogRepository(
|
||||
updateCallLinkRepository.deleteCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Update) 0 else 1)
|
||||
acc + (if (current is UpdateCallLinkResult.Delete) 0 else 1)
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.signal.core.util.delete
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSet
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.readToSingleLong
|
||||
import org.signal.core.util.readToSingleObject
|
||||
@@ -18,7 +17,6 @@ import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
@@ -34,6 +32,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@@ -55,6 +54,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
const val REVOKED = "revoked"
|
||||
const val EXPIRATION = "expiration"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
|
||||
//language=sql
|
||||
const val CREATE_TABLE = """
|
||||
@@ -67,7 +67,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
$RESTRICTIONS INTEGER NOT NULL,
|
||||
$REVOKED INTEGER NOT NULL,
|
||||
$EXPIRATION INTEGER NOT NULL,
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
|
||||
$DELETION_TIMESTAMP INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -90,14 +91,23 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
|
||||
fun insertCallLink(
|
||||
callLink: CallLink
|
||||
callLink: CallLink,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: StorageId? = null
|
||||
): RecipientId {
|
||||
val recipientId: RecipientId = writableDatabase.withinTransaction { db ->
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId)
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId, storageId = storageId?.raw)
|
||||
|
||||
val contentValues = CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId)).apply {
|
||||
put(DELETION_TIMESTAMP, deletionTimestamp)
|
||||
if (deletionTimestamp > 0) {
|
||||
put(REVOKED, true)
|
||||
}
|
||||
}
|
||||
|
||||
db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId)))
|
||||
.values(contentValues)
|
||||
.run()
|
||||
|
||||
recipientId
|
||||
@@ -190,7 +200,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
|
||||
insertCallLink(link)
|
||||
@@ -200,6 +211,62 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
}
|
||||
|
||||
fun insertOrUpdateCallLinkByRootKey(
|
||||
callLinkRootKey: CallLinkRootKey,
|
||||
adminPassKey: ByteArray?,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: StorageId? = null
|
||||
) {
|
||||
val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId())
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val callLink = getCallLinkByRoomId(roomId)
|
||||
if (callLink == null) {
|
||||
val link = CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = roomId,
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
adminPassBytes = adminPassKey
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
|
||||
insertCallLink(link, deletionTimestamp, storageId)
|
||||
} else {
|
||||
if (storageId != null) {
|
||||
SignalDatabase.recipients.updateStorageId(callLink.recipientId, storageId.raw)
|
||||
}
|
||||
|
||||
if (deletionTimestamp != 0L) {
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(
|
||||
DELETION_TIMESTAMP to deletionTimestamp,
|
||||
ADMIN_KEY to null,
|
||||
REVOKED to true
|
||||
)
|
||||
.where("$ROOM_ID = ?", callLink.roomId.serialize())
|
||||
.run()
|
||||
} else 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unix timestamp, or 0
|
||||
*/
|
||||
fun getDeletedTimestampByRoomId(
|
||||
roomId: CallLinkRoomId
|
||||
): Long {
|
||||
return readableDatabase
|
||||
.select(DELETION_TIMESTAMP)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ROOM_ID = ?", roomId.serialize())
|
||||
.run()
|
||||
.readToSingleLong(defaultValue = 0)
|
||||
}
|
||||
|
||||
fun getOrCreateCallLinkByRoomId(
|
||||
callLinkRoomId: CallLinkRoomId
|
||||
): CallLink {
|
||||
@@ -209,7 +276,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = callLinkRoomId,
|
||||
credentials = null,
|
||||
state = SignalCallLinkState()
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
insertCallLink(link)
|
||||
return getCallLinkByRoomId(callLinkRoomId)!!
|
||||
@@ -235,6 +303,15 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
}
|
||||
|
||||
fun getAll(): List<CallLink> {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.run()
|
||||
.readToList {
|
||||
CallLinkDeserializer.deserialize(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the call link into the "revoked" state which will hide it from the UI and
|
||||
* delete it after a few days.
|
||||
@@ -244,25 +321,20 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.update(TABLE_NAME)
|
||||
.values(REVOKED to true)
|
||||
.values(
|
||||
REVOKED to true,
|
||||
DELETION_TIMESTAMP to System.currentTimeMillis(),
|
||||
ADMIN_KEY to null
|
||||
)
|
||||
.where("$ROOM_ID = ?", roomId.serialize())
|
||||
.run()
|
||||
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}
|
||||
}
|
||||
val recipient = SignalDatabase.recipients.getByCallLinkRoomId(roomId)
|
||||
|
||||
/**
|
||||
* Deletes the call link. This should only happen *after* we send out a sync message
|
||||
* or receive a sync message which deletes the corresponding link.
|
||||
*/
|
||||
fun deleteCallLink(
|
||||
roomId: CallLinkRoomId
|
||||
) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.delete(TABLE_NAME)
|
||||
.where("$ROOM_ID = ?", roomId.serialize())
|
||||
.run()
|
||||
if (recipient.isPresent) {
|
||||
SignalDatabase.recipients.markNeedsSync(recipient.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +351,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
fun deleteNonAdminCallLinksOnOrBefore(timestamp: Long) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.delete(TABLE_NAME)
|
||||
.where("EXISTS (SELECT 1 FROM ${CallTable.TABLE_NAME} WHERE ${CallTable.TIMESTAMP} <= ? AND ${CallTable.PEER} = $RECIPIENT_ID)", timestamp)
|
||||
.where("$ADMIN_KEY IS NULL AND EXISTS (SELECT 1 FROM ${CallTable.TABLE_NAME} WHERE ${CallTable.TIMESTAMP} <= ? AND ${CallTable.PEER} = $RECIPIENT_ID)", timestamp)
|
||||
.run()
|
||||
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps(skipSync = true)
|
||||
@@ -334,18 +406,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
}
|
||||
|
||||
fun getAdminCallLinkCredentialsOnOrBefore(timestamp: Long): Set<CallLinkCredentials> {
|
||||
val query = """
|
||||
SELECT $ROOT_KEY, $ADMIN_KEY FROM $TABLE_NAME
|
||||
INNER JOIN ${CallTable.TABLE_NAME} ON ${CallTable.TABLE_NAME}.${CallTable.PEER} = $TABLE_NAME.$RECIPIENT_ID
|
||||
WHERE ${CallTable.TIMESTAMP} <= $timestamp AND $ADMIN_KEY IS NOT NULL AND $REVOKED = 0
|
||||
""".trimIndent()
|
||||
|
||||
return readableDatabase.query(query).readToSet {
|
||||
CallLinkCredentials(it.requireNonNullBlob(ROOT_KEY), it.requireNonNullBlob(ADMIN_KEY))
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryCallLinks(query: String?, offset: Int, limit: Int, asCount: Boolean): Cursor {
|
||||
//language=sql
|
||||
val noCallEvent = """
|
||||
@@ -381,7 +441,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
val statement = """
|
||||
SELECT $projection
|
||||
FROM $TABLE_NAME
|
||||
WHERE $noCallEvent AND NOT $REVOKED ${searchFilter?.where ?: ""} AND $ROOT_KEY IS NOT NULL
|
||||
WHERE $noCallEvent AND NOT $REVOKED ${searchFilter?.where ?: ""} AND $ROOT_KEY IS NOT NULL AND $DELETION_TIMESTAMP = 0
|
||||
ORDER BY $ID DESC
|
||||
$limitOffset
|
||||
""".trimIndent()
|
||||
@@ -432,7 +492,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
Instant.ofEpochMilli(it).truncatedTo(ChronoUnit.DAYS)
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
deletionTimestamp = data.requireLong(DELETION_TIMESTAMP)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -449,7 +510,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
val recipientId: RecipientId,
|
||||
val roomId: CallLinkRoomId,
|
||||
val credentials: CallLinkCredentials?,
|
||||
val state: SignalCallLinkState
|
||||
val state: SignalCallLinkState,
|
||||
val deletionTimestamp: Long
|
||||
) {
|
||||
val avatarColor: AvatarColor = credentials?.let { AvatarColorHash.forCallLink(it.linkKeyBytes) } ?: AvatarColor.UNKNOWN
|
||||
}
|
||||
|
||||
@@ -574,13 +574,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
).recipientId
|
||||
}
|
||||
|
||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
|
||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId, storageId: ByteArray? = null): RecipientId {
|
||||
return getOrInsertByColumn(
|
||||
CALL_LINK_ROOM_ID,
|
||||
callLinkRoomId.serialize(),
|
||||
contentValuesOf(
|
||||
TYPE to RecipientType.CALL_LINK.id,
|
||||
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
|
||||
STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId ?: StorageSyncHelper.generateKey()),
|
||||
PROFILE_SHARING to 1
|
||||
)
|
||||
).recipientId
|
||||
@@ -1199,6 +1200,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
SELECT ${DistributionListTables.ListTable.ID}
|
||||
FROM ${DistributionListTables.ListTable.TABLE_NAME}
|
||||
)
|
||||
OR
|
||||
$CALL_LINK_ROOM_ID NOT NULL AND $CALL_LINK_ROOM_ID IN (
|
||||
SELECT ${CallLinkTable.ROOM_ID}
|
||||
FROM ${CallLinkTable.TABLE_NAME}
|
||||
WHERE (${CallLinkTable.ADMIN_KEY} NOT NULL OR ${CallLinkTable.DELETION_TIMESTAMP} > 0) AND ${CallLinkTable.ROOT_KEY} NOT NULL
|
||||
)
|
||||
)
|
||||
""",
|
||||
RecipientType.INDIVIDUAL.id,
|
||||
@@ -1217,6 +1224,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
RecipientType.INDIVIDUAL -> out[id] = StorageId.forContact(key)
|
||||
RecipientType.GV1 -> out[id] = StorageId.forGroupV1(key)
|
||||
RecipientType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
|
||||
RecipientType.CALL_LINK -> out[id] = StorageId.forCallLink(key)
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
@@ -3870,8 +3878,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
|
||||
}
|
||||
|
||||
val query = "$ID = ? AND ($TYPE IN (?, ?, ?) OR $REGISTERED = ? OR $ID = ?)"
|
||||
val args = SqlUtil.buildArgs(recipientId, RecipientType.GV1.id, RecipientType.GV2.id, RecipientType.DISTRIBUTION_LIST.id, RegisteredState.REGISTERED.id, selfId.toLong())
|
||||
val query = "$ID = ? AND ($TYPE IN (?, ?, ?, ?) OR $REGISTERED = ? OR $ID = ?)"
|
||||
val args = SqlUtil.buildArgs(recipientId, RecipientType.GV1.id, RecipientType.GV2.id, RecipientType.DISTRIBUTION_LIST.id, RecipientType.CALL_LINK.id, RegisteredState.REGISTERED.id, selfId.toLong())
|
||||
|
||||
writableDatabase.update(TABLE_NAME, values, query, args).also { updateCount ->
|
||||
Log.d(TAG, "[rotateStorageId] updateCount: $updateCount")
|
||||
|
||||
@@ -102,6 +102,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V241_ExpireTimerVer
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V242_MessageFullTextSearchEmojiSupportV2
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V243_MessageFullTextSearchDisableSecureDelete
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V244_AttachmentRemoteIv
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V245_DeletionTimestampOnCallLinks
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -206,10 +207,11 @@ object SignalDatabaseMigrations {
|
||||
241 to V241_ExpireTimerVersion,
|
||||
242 to V242_MessageFullTextSearchEmojiSupportV2,
|
||||
243 to V243_MessageFullTextSearchDisableSecureDelete,
|
||||
244 to V244_AttachmentRemoteIv
|
||||
244 to V244_AttachmentRemoteIv,
|
||||
245 to V245_DeletionTimestampOnCallLinks
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 244
|
||||
const val DATABASE_VERSION = 245
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Adds a deletion timestamp to the call links table, which is required for storage service syncing.
|
||||
*/
|
||||
object V245_DeletionTimestampOnCallLinks : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE call_link ADD COLUMN deletion_timestamp INTEGER DEFAULT 0 NOT NULL;")
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ class CallLinkUpdateSendJob private constructor(
|
||||
.type(
|
||||
when (callLinkUpdateType) {
|
||||
CallLinkUpdate.Type.UPDATE -> CallLinkUpdateSendJobData.Type.UPDATE
|
||||
CallLinkUpdate.Type.DELETE -> CallLinkUpdateSendJobData.Type.DELETE
|
||||
}
|
||||
)
|
||||
.build()
|
||||
@@ -83,10 +82,6 @@ class CallLinkUpdateSendJob private constructor(
|
||||
|
||||
AppDependencies.signalServiceMessageSender
|
||||
.sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate))
|
||||
|
||||
if (callLinkUpdateType == CallLinkUpdate.Type.DELETE) {
|
||||
SignalDatabase.callLinks.deleteCallLink(callLinkRoomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean {
|
||||
@@ -102,7 +97,6 @@ class CallLinkUpdateSendJob private constructor(
|
||||
val jobData = CallLinkUpdateSendJobData.ADAPTER.decode(serializedData!!)
|
||||
val type: CallLinkUpdate.Type = when (jobData.type) {
|
||||
CallLinkUpdateSendJobData.Type.UPDATE, null -> CallLinkUpdate.Type.UPDATE
|
||||
CallLinkUpdateSendJobData.Type.DELETE -> CallLinkUpdate.Type.DELETE
|
||||
}
|
||||
|
||||
return CallLinkUpdateSendJob(
|
||||
|
||||
@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJo
|
||||
import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.SubscriberIdMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.Svr2MirrorMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.SyncCallLinksMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.SyncKeysMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
|
||||
@@ -299,6 +300,7 @@ public final class JobManagerFactories {
|
||||
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
|
||||
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
|
||||
put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory());
|
||||
put(SyncCallLinksMigrationJob.KEY, new SyncCallLinksMigrationJob.Factory());
|
||||
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
||||
put(SyncKeysMigrationJob.KEY, new SyncKeysMigrationJob.Factory());
|
||||
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.AccountRecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.CallLinkRecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.ContactRecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor;
|
||||
@@ -42,6 +43,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
@@ -287,7 +289,7 @@ public class StorageSyncJob extends BaseJob {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
Log.i(TAG, "[Remote Sync] Remote-Only :: Contacts: " + remoteOnly.contacts.size() + ", GV1: " + remoteOnly.gv1.size() + ", GV2: " + remoteOnly.gv2.size() + ", Account: " + remoteOnly.account.size() + ", DLists: " + remoteOnly.storyDistributionLists.size());
|
||||
Log.i(TAG, "[Remote Sync] Remote-Only :: Contacts: " + remoteOnly.contacts.size() + ", GV1: " + remoteOnly.gv1.size() + ", GV2: " + remoteOnly.gv2.size() + ", Account: " + remoteOnly.account.size() + ", DLists: " + remoteOnly.storyDistributionLists.size() + ", call links: " + remoteOnly.callLinkRecords.size());
|
||||
|
||||
processKnownRecords(context, remoteOnly);
|
||||
|
||||
@@ -411,6 +413,7 @@ public class StorageSyncJob extends BaseJob {
|
||||
new GroupV2RecordProcessor(context).process(records.gv2, StorageSyncHelper.KEY_GENERATOR);
|
||||
new AccountRecordProcessor(context, freshSelf()).process(records.account, StorageSyncHelper.KEY_GENERATOR);
|
||||
new StoryDistributionListRecordProcessor().process(records.storyDistributionLists, StorageSyncHelper.KEY_GENERATOR);
|
||||
new CallLinkRecordProcessor().process(records.callLinkRecords, StorageSyncHelper.KEY_GENERATOR);
|
||||
}
|
||||
|
||||
private static @NonNull List<StorageId> getAllLocalStorageIds(@NonNull Recipient self) {
|
||||
@@ -468,6 +471,18 @@ public class StorageSyncJob extends BaseJob {
|
||||
throw new MissingRecipientModelError("Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
case CALL_LINK:
|
||||
RecipientRecord callLinkRecord = recipientTable.getByStorageId(id.getRaw());
|
||||
if (callLinkRecord != null) {
|
||||
if (callLinkRecord.getCallLinkRoomId() != null) {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(callLinkRecord));
|
||||
} else {
|
||||
throw new MissingRecipientModelError("Missing local recipient model (no CallLinkRoomId)! Type: " + id.getType());
|
||||
}
|
||||
} else {
|
||||
throw new MissingRecipientModelError("Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
||||
if (unknown != null) {
|
||||
@@ -501,6 +516,7 @@ public class StorageSyncJob extends BaseJob {
|
||||
final List<SignalAccountRecord> account = new LinkedList<>();
|
||||
final List<SignalStorageRecord> unknown = new LinkedList<>();
|
||||
final List<SignalStoryDistributionListRecord> storyDistributionLists = new LinkedList<>();
|
||||
final List<SignalCallLinkRecord> callLinkRecords = new LinkedList<>();
|
||||
|
||||
StorageRecordCollection(Collection<SignalStorageRecord> records) {
|
||||
for (SignalStorageRecord record : records) {
|
||||
@@ -514,6 +530,8 @@ public class StorageSyncJob extends BaseJob {
|
||||
account.add(record.getAccount().get());
|
||||
} else if (record.getStoryDistributionList().isPresent()) {
|
||||
storyDistributionLists.add(record.getStoryDistributionList().get());
|
||||
} else if (record.getCallLink().isPresent()) {
|
||||
callLinkRecords.add(record.getCallLink().get());
|
||||
} else if (record.getId().isUnknown()) {
|
||||
unknown.add(record);
|
||||
} else {
|
||||
|
||||
@@ -1336,12 +1336,6 @@ object SyncMessageProcessor {
|
||||
}
|
||||
|
||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(callLinkRootKey)
|
||||
if (callLinkUpdate.type == CallLinkUpdate.Type.DELETE) {
|
||||
log(envelopeTimestamp, "Synchronize call link deletion.")
|
||||
SignalDatabase.callLinks.deleteCallLink(roomId)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalDatabase.callLinks.callLinkExists(roomId)) {
|
||||
log(envelopeTimestamp, "Synchronize call link for a link we already know about. Updating credentials.")
|
||||
@@ -1362,9 +1356,12 @@ object SyncMessageProcessor {
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
adminPassBytes = callLinkUpdate.adminPassKey?.toByteArray()
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
)
|
||||
)
|
||||
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
AppDependencies.jobManager.add(RefreshCallLinkDetailsJob(callLinkUpdate))
|
||||
|
||||
@@ -155,9 +155,10 @@ public class ApplicationMigrations {
|
||||
static final int EXPIRE_TIMER_CAPABILITY_2 = 111;
|
||||
// static final int BACKFILL_DIGESTS = 112;
|
||||
static final int BACKFILL_DIGESTS_V2 = 113;
|
||||
static final int CALL_LINK_STORAGE_SYNC = 114;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 113;
|
||||
public static final int CURRENT_VERSION = 114;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -708,6 +709,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.BACKFILL_DIGESTS_V2, new BackfillDigestsMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.CALL_LINK_STORAGE_SYNC) {
|
||||
jobs.put(Version.CALL_LINK_STORAGE_SYNC, new SyncCallLinksMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.migrations
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
|
||||
/**
|
||||
* Marks all call links as needing to be synced by storage service.
|
||||
*/
|
||||
internal class SyncCallLinksMigrationJob @JvmOverloads constructor(parameters: Parameters = Parameters.Builder().build()) : MigrationJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "SyncCallLinksMigrationJob"
|
||||
|
||||
private val TAG = Log.tag(SyncCallLinksMigrationJob::class)
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun isUiBlocking(): Boolean = false
|
||||
|
||||
override fun performMigration() {
|
||||
if (SignalStore.account.aci == null) {
|
||||
Log.w(TAG, "Self not available yet.")
|
||||
return
|
||||
}
|
||||
|
||||
val callLinkRecipients = SignalDatabase.callLinks.getAll().map { it.recipientId }.filter {
|
||||
try {
|
||||
Recipient.resolved(it)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to resolve recipient: $it")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(callLinkRecipients)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
override fun shouldRetry(e: Exception): Boolean = false
|
||||
|
||||
class Factory : Job.Factory<SyncCallLinksMigrationJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): SyncCallLinksMigrationJob {
|
||||
return SyncCallLinksMigrationJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import java.util.Optional
|
||||
|
||||
internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkRecord>() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallLinkRecordProcessor::class)
|
||||
}
|
||||
|
||||
override fun compare(o1: SignalCallLinkRecord?, o2: SignalCallLinkRecord?): Int {
|
||||
return if (o1?.rootKey.contentEquals(o2?.rootKey)) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
internal override fun isInvalid(remote: SignalCallLinkRecord): Boolean {
|
||||
return remote.adminPassKey.isNotEmpty() && remote.deletionTimestamp > 0L
|
||||
}
|
||||
|
||||
internal override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional<SignalCallLinkRecord> {
|
||||
Log.d(TAG, "Attempting to get matching record...")
|
||||
val rootKey = CallLinkRootKey(remote.rootKey)
|
||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey)
|
||||
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(roomId)
|
||||
if (callLink != null && callLink.credentials?.adminPassBytes != null) {
|
||||
val builder = SignalCallLinkRecord.Builder(keyGenerator.generate(), null).apply {
|
||||
setRootKey(rootKey.keyBytes)
|
||||
setAdminPassKey(callLink.credentials.adminPassBytes)
|
||||
setDeletedTimestamp(callLink.deletionTimestamp)
|
||||
}
|
||||
return Optional.of(builder.build())
|
||||
} else {
|
||||
return Optional.empty<SignalCallLinkRecord>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A deleted record takes precedence over a non-deleted record
|
||||
* An earlier deletion takes precedence over a later deletion
|
||||
* Other fields should not change, except for the clearing of the admin passkey on deletion
|
||||
*/
|
||||
internal override fun merge(remote: SignalCallLinkRecord, local: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): SignalCallLinkRecord {
|
||||
return if (remote.isDeleted() && local.isDeleted()) {
|
||||
if (remote.deletionTimestamp < local.deletionTimestamp) {
|
||||
remote
|
||||
} else {
|
||||
local
|
||||
}
|
||||
} else if (remote.isDeleted()) {
|
||||
remote
|
||||
} else if (local.isDeleted()) {
|
||||
local
|
||||
} else {
|
||||
remote
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertLocal(record: SignalCallLinkRecord) {
|
||||
insertOrUpdateRecord(record)
|
||||
}
|
||||
|
||||
override fun updateLocal(update: StorageRecordUpdate<SignalCallLinkRecord>) {
|
||||
insertOrUpdateRecord(update.new)
|
||||
}
|
||||
|
||||
private fun insertOrUpdateRecord(record: SignalCallLinkRecord) {
|
||||
val rootKey = CallLinkRootKey(record.rootKey)
|
||||
|
||||
SignalDatabase.callLinks.insertOrUpdateCallLinkByRootKey(
|
||||
callLinkRootKey = rootKey,
|
||||
adminPassKey = record.adminPassKey,
|
||||
deletionTimestamp = record.deletionTimestamp,
|
||||
storageId = record.id
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme;
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.IdentityTable;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
@@ -20,8 +21,10 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
@@ -35,6 +38,7 @@ import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class StorageSyncModels {
|
||||
@@ -63,6 +67,7 @@ public final class StorageSyncModels {
|
||||
case GV1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||
case GV2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
||||
case DISTRIBUTION_LIST: return SignalStorageRecord.forStoryDistributionList(localToRemoteStoryDistributionList(settings, rawStorageId));
|
||||
case CALL_LINK: return SignalStorageRecord.forCallLink(localToRemoteCallLink(settings, rawStorageId));
|
||||
default: throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
@@ -226,6 +231,32 @@ public final class StorageSyncModels {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalCallLinkRecord localToRemoteCallLink(@NonNull RecipientRecord recipient, @NonNull byte[] rawStorageId) {
|
||||
CallLinkRoomId callLinkRoomId = recipient.getCallLinkRoomId();
|
||||
|
||||
if (callLinkRoomId == null) {
|
||||
throw new AssertionError("Must have a callLinkRoomId!");
|
||||
}
|
||||
|
||||
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getCallLinkByRoomId(callLinkRoomId);
|
||||
if (callLink == null) {
|
||||
throw new AssertionError("Must have a call link record!");
|
||||
}
|
||||
|
||||
if (callLink.getCredentials() == null) {
|
||||
throw new AssertionError("Must have call link credentials!");
|
||||
}
|
||||
|
||||
long deletedTimestamp = Math.max(0, SignalDatabase.callLinks().getDeletedTimestampByRoomId(callLinkRoomId));
|
||||
byte[] adminPassword = deletedTimestamp > 0 ? new byte[]{} : Objects.requireNonNull(callLink.getCredentials().getAdminPassBytes(), "Non-deleted call link requires admin pass!");
|
||||
|
||||
return new SignalCallLinkRecord.Builder(rawStorageId, null)
|
||||
.setRootKey(callLink.getCredentials().getLinkKeyBytes())
|
||||
.setAdminPassKey(adminPassword)
|
||||
.setDeletedTimestamp(deletedTimestamp)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalStoryDistributionListRecord localToRemoteStoryDistributionList(@NonNull RecipientRecord recipient, @NonNull byte[] rawStorageId) {
|
||||
DistributionListId distributionListId = recipient.getDistributionListId();
|
||||
|
||||
|
||||
@@ -144,6 +144,11 @@ public final class StorageSyncValidations {
|
||||
throw new DuplicateDistributionListIdError();
|
||||
}
|
||||
|
||||
ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.CALL_LINK.getValue());
|
||||
if (ids.size() != new HashSet<>(ids).size()) {
|
||||
throw new DuplicateCallLinkError();
|
||||
}
|
||||
|
||||
throw new DuplicateRawIdAcrossTypesError();
|
||||
}
|
||||
|
||||
@@ -196,6 +201,9 @@ public final class StorageSyncValidations {
|
||||
private static final class DuplicateDistributionListIdError extends Error {
|
||||
}
|
||||
|
||||
private static final class DuplicateCallLinkError extends Error {
|
||||
}
|
||||
|
||||
private static final class DuplicateInsertInWriteError extends Error {
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ message CallLogEventSendJobData {
|
||||
message CallLinkUpdateSendJobData {
|
||||
enum Type {
|
||||
UPDATE = 0;
|
||||
DELETE = 1;
|
||||
reserved 1; // was DELETE, superseded by storage service
|
||||
}
|
||||
|
||||
string callLinkRoomId = 1;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import okio.ByteString.Companion.EMPTY
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.testutil.EmptyLogger
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
|
||||
|
||||
/**
|
||||
* See [CallLinkRecordProcessor]
|
||||
*/
|
||||
class CallLinkRecordProcessorTest {
|
||||
companion object {
|
||||
val STORAGE_ID: StorageId = StorageId.forCallLink(byteArrayOf(1, 2, 3, 4))
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpClass() {
|
||||
Log.initialize(EmptyLogger())
|
||||
}
|
||||
}
|
||||
|
||||
private val testSubject = CallLinkRecordProcessor()
|
||||
private val mockCredentials = CallLinkCredentials(
|
||||
"root key".toByteArray(),
|
||||
"admin pass".toByteArray()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Given a valid proto with only an admin pass key and not a deletion timestamp, assert valid`() {
|
||||
// GIVEN
|
||||
val proto = CallLinkRecord.Builder().apply {
|
||||
rootKey = mockCredentials.linkKeyBytes.toByteString()
|
||||
adminPasskey = mockCredentials.adminPassBytes!!.toByteString()
|
||||
deletedAtTimestampMs = 0L
|
||||
}.build()
|
||||
|
||||
val record = SignalCallLinkRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a valid proto with only a deletion timestamp and not an admin pass key, assert valid`() {
|
||||
// GIVEN
|
||||
val proto = CallLinkRecord.Builder().apply {
|
||||
rootKey = mockCredentials.linkKeyBytes.toByteString()
|
||||
adminPasskey = EMPTY
|
||||
deletedAtTimestampMs = System.currentTimeMillis()
|
||||
}.build()
|
||||
|
||||
val record = SignalCallLinkRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto with both an admin pass key and a deletion timestamp, assert invalid`() {
|
||||
// GIVEN
|
||||
val proto = CallLinkRecord.Builder().apply {
|
||||
rootKey = mockCredentials.linkKeyBytes.toByteString()
|
||||
adminPasskey = mockCredentials.adminPassBytes!!.toByteString()
|
||||
deletedAtTimestampMs = System.currentTimeMillis()
|
||||
}.build()
|
||||
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
SignalCallLinkRecord(STORAGE_ID, proto)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto with neither an admin pass key nor a deletion timestamp, assert valid`() {
|
||||
// GIVEN
|
||||
val proto = CallLinkRecord.Builder().apply {
|
||||
rootKey = mockCredentials.linkKeyBytes.toByteString()
|
||||
adminPasskey = EMPTY
|
||||
deletedAtTimestampMs = 0L
|
||||
}.build()
|
||||
|
||||
val record = SignalCallLinkRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user