Add call link support to storage service.

This commit is contained in:
Nicholas Tinsley
2024-09-09 13:15:32 -04:00
committed by Cody Henthorne
parent 1f2b5e90a3
commit e247d311d8
29 changed files with 645 additions and 83 deletions

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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;")
}
}