mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 17:29:02 +01: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,
|
linkKeyBytes = roomId,
|
||||||
adminPassBytes = null
|
adminPassBytes = null
|
||||||
),
|
),
|
||||||
state = SignalCallLinkState()
|
state = SignalCallLinkState(),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -47,7 +47,8 @@ fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId? {
|
|||||||
name = callLink.name,
|
name = callLink.name,
|
||||||
restrictions = callLink.restrictions.toLocal(),
|
restrictions = callLink.restrictions.toLocal(),
|
||||||
expiration = Instant.ofEpochMilli(callLink.expirationMs)
|
expiration = Instant.ofEpochMilli(callLink.expirationMs)
|
||||||
)
|
),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ private fun SignalCallRowPreview() {
|
|||||||
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
|
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
|
||||||
expiration = Instant.MAX,
|
expiration = Instant.MAX,
|
||||||
revoked = false
|
revoked = false
|
||||||
)
|
),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Previews.Preview {
|
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.CallLinkCredentials
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository for performing update operations on call links:
|
* Repository for performing update operations on call links:
|
||||||
@@ -65,6 +66,7 @@ class UpdateCallLinkRepository(
|
|||||||
is UpdateCallLinkResult.Delete -> {
|
is UpdateCallLinkResult.Delete -> {
|
||||||
SignalDatabase.callLinks.markRevoked(credentials.roomId)
|
SignalDatabase.callLinks.markRevoked(credentials.roomId)
|
||||||
AppDependencies.jobManager.add(CallLinkUpdateSendJob(credentials.roomId))
|
AppDependencies.jobManager.add(CallLinkUpdateSendJob(credentials.roomId))
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -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.CallLinkCredentials
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,22 +41,25 @@ class CreateCallLinkRepository(
|
|||||||
SignalDatabase.callLinks.insertCallLink(
|
SignalDatabase.callLinks.insertCallLink(
|
||||||
CallLinkTable.CallLink(
|
CallLinkTable.CallLink(
|
||||||
recipientId = RecipientId.UNKNOWN,
|
recipientId = RecipientId.UNKNOWN,
|
||||||
roomId = credentials.roomId,
|
roomId = it.credentials.roomId,
|
||||||
credentials = credentials,
|
credentials = it.credentials,
|
||||||
state = it.state
|
state = it.state,
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
AppDependencies.jobManager.add(
|
AppDependencies.jobManager.add(
|
||||||
CallLinkUpdateSendJob(
|
CallLinkUpdateSendJob(
|
||||||
credentials.roomId,
|
it.credentials.roomId,
|
||||||
SyncMessage.CallLinkUpdate.Type.UPDATE
|
SyncMessage.CallLinkUpdate.Type.UPDATE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
|
||||||
EnsureCallLinkCreatedResult.Success(
|
EnsureCallLinkCreatedResult.Success(
|
||||||
Recipient.resolved(
|
Recipient.resolved(
|
||||||
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId).get()
|
SignalDatabase.recipients.getByCallLinkRoomId(it.credentials.roomId).get()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ class CreateCallLinkViewModel(
|
|||||||
restrictions = Restrictions.ADMIN_APPROVAL,
|
restrictions = Restrictions.ADMIN_APPROVAL,
|
||||||
revoked = false,
|
revoked = false,
|
||||||
expiration = Instant.MAX
|
expiration = Instant.MAX
|
||||||
)
|
),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -140,7 +140,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
|||||||
viewModel.setDisplayRevocationDialog(false)
|
viewModel.setDisplayRevocationDialog(false)
|
||||||
lifecycleDisposable += viewModel.delete().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
lifecycleDisposable += viewModel.delete().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is UpdateCallLinkResult.Update -> ActivityCompat.finishAfterTransition(requireActivity())
|
is UpdateCallLinkResult.Delete -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Failed to revoke. $it")
|
Log.w(TAG, "Failed to revoke. $it")
|
||||||
toastFailure()
|
toastFailure()
|
||||||
@@ -213,7 +213,8 @@ private fun CallLinkDetailsPreview() {
|
|||||||
revoked = false,
|
revoked = false,
|
||||||
restrictions = Restrictions.NONE,
|
restrictions = Restrictions.NONE,
|
||||||
expiration = Instant.MAX
|
expiration = Instant.MAX
|
||||||
)
|
),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class CallLogRepository(
|
|||||||
updateCallLinkRepository.deleteCallLink(it.credentials!!)
|
updateCallLinkRepository.deleteCallLink(it.credentials!!)
|
||||||
}
|
}
|
||||||
).reduce(0) { acc, current ->
|
).reduce(0) { acc, current ->
|
||||||
acc + (if (current is UpdateCallLinkResult.Update) 0 else 1)
|
acc + (if (current is UpdateCallLinkResult.Delete) 0 else 1)
|
||||||
}.doOnTerminate {
|
}.doOnTerminate {
|
||||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||||
}.doOnDispose {
|
}.doOnDispose {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.signal.core.util.delete
|
|||||||
import org.signal.core.util.insertInto
|
import org.signal.core.util.insertInto
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.readToList
|
import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.readToSet
|
|
||||||
import org.signal.core.util.readToSingleInt
|
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
|
||||||
@@ -18,7 +17,6 @@ import org.signal.core.util.requireBlob
|
|||||||
import org.signal.core.util.requireBoolean
|
import org.signal.core.util.requireBoolean
|
||||||
import org.signal.core.util.requireInt
|
import org.signal.core.util.requireInt
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullBlob
|
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
import org.signal.core.util.update
|
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.CallLinkCredentials
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
@@ -55,6 +54,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
const val REVOKED = "revoked"
|
const val REVOKED = "revoked"
|
||||||
const val EXPIRATION = "expiration"
|
const val EXPIRATION = "expiration"
|
||||||
const val RECIPIENT_ID = "recipient_id"
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
|
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||||
|
|
||||||
//language=sql
|
//language=sql
|
||||||
const val CREATE_TABLE = """
|
const val CREATE_TABLE = """
|
||||||
@@ -67,7 +67,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
$RESTRICTIONS INTEGER NOT NULL,
|
$RESTRICTIONS INTEGER NOT NULL,
|
||||||
$REVOKED INTEGER NOT NULL,
|
$REVOKED INTEGER NOT NULL,
|
||||||
$EXPIRATION 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(
|
fun insertCallLink(
|
||||||
callLink: CallLink
|
callLink: CallLink,
|
||||||
|
deletionTimestamp: Long = 0L,
|
||||||
|
storageId: StorageId? = null
|
||||||
): RecipientId {
|
): RecipientId {
|
||||||
val recipientId: RecipientId = writableDatabase.withinTransaction { db ->
|
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
|
db
|
||||||
.insertInto(TABLE_NAME)
|
.insertInto(TABLE_NAME)
|
||||||
.values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId)))
|
.values(contentValues)
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
recipientId
|
recipientId
|
||||||
@@ -190,7 +200,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||||
adminPassBytes = null
|
adminPassBytes = null
|
||||||
),
|
),
|
||||||
state = SignalCallLinkState()
|
state = SignalCallLinkState(),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
|
|
||||||
insertCallLink(link)
|
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(
|
fun getOrCreateCallLinkByRoomId(
|
||||||
callLinkRoomId: CallLinkRoomId
|
callLinkRoomId: CallLinkRoomId
|
||||||
): CallLink {
|
): CallLink {
|
||||||
@@ -209,7 +276,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
recipientId = RecipientId.UNKNOWN,
|
recipientId = RecipientId.UNKNOWN,
|
||||||
roomId = callLinkRoomId,
|
roomId = callLinkRoomId,
|
||||||
credentials = null,
|
credentials = null,
|
||||||
state = SignalCallLinkState()
|
state = SignalCallLinkState(),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
insertCallLink(link)
|
insertCallLink(link)
|
||||||
return getCallLinkByRoomId(callLinkRoomId)!!
|
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
|
* Puts the call link into the "revoked" state which will hide it from the UI and
|
||||||
* delete it after a few days.
|
* delete it after a few days.
|
||||||
@@ -244,25 +321,20 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
) {
|
) {
|
||||||
writableDatabase.withinTransaction { db ->
|
writableDatabase.withinTransaction { db ->
|
||||||
db.update(TABLE_NAME)
|
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())
|
.where("$ROOM_ID = ?", roomId.serialize())
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||||
}
|
val recipient = SignalDatabase.recipients.getByCallLinkRoomId(roomId)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (recipient.isPresent) {
|
||||||
* Deletes the call link. This should only happen *after* we send out a sync message
|
SignalDatabase.recipients.markNeedsSync(recipient.get())
|
||||||
* 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +351,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
fun deleteNonAdminCallLinksOnOrBefore(timestamp: Long) {
|
fun deleteNonAdminCallLinksOnOrBefore(timestamp: Long) {
|
||||||
writableDatabase.withinTransaction { db ->
|
writableDatabase.withinTransaction { db ->
|
||||||
db.delete(TABLE_NAME)
|
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()
|
.run()
|
||||||
|
|
||||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps(skipSync = true)
|
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 {
|
private fun queryCallLinks(query: String?, offset: Int, limit: Int, asCount: Boolean): Cursor {
|
||||||
//language=sql
|
//language=sql
|
||||||
val noCallEvent = """
|
val noCallEvent = """
|
||||||
@@ -381,7 +441,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
val statement = """
|
val statement = """
|
||||||
SELECT $projection
|
SELECT $projection
|
||||||
FROM $TABLE_NAME
|
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
|
ORDER BY $ID DESC
|
||||||
$limitOffset
|
$limitOffset
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@@ -432,7 +492,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||||||
Instant.ofEpochMilli(it).truncatedTo(ChronoUnit.DAYS)
|
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 recipientId: RecipientId,
|
||||||
val roomId: CallLinkRoomId,
|
val roomId: CallLinkRoomId,
|
||||||
val credentials: CallLinkCredentials?,
|
val credentials: CallLinkCredentials?,
|
||||||
val state: SignalCallLinkState
|
val state: SignalCallLinkState,
|
||||||
|
val deletionTimestamp: Long
|
||||||
) {
|
) {
|
||||||
val avatarColor: AvatarColor = credentials?.let { AvatarColorHash.forCallLink(it.linkKeyBytes) } ?: AvatarColor.UNKNOWN
|
val avatarColor: AvatarColor = credentials?.let { AvatarColorHash.forCallLink(it.linkKeyBytes) } ?: AvatarColor.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -574,13 +574,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
).recipientId
|
).recipientId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
|
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId, storageId: ByteArray? = null): RecipientId {
|
||||||
return getOrInsertByColumn(
|
return getOrInsertByColumn(
|
||||||
CALL_LINK_ROOM_ID,
|
CALL_LINK_ROOM_ID,
|
||||||
callLinkRoomId.serialize(),
|
callLinkRoomId.serialize(),
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
TYPE to RecipientType.CALL_LINK.id,
|
TYPE to RecipientType.CALL_LINK.id,
|
||||||
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
|
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
|
||||||
|
STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId ?: StorageSyncHelper.generateKey()),
|
||||||
PROFILE_SHARING to 1
|
PROFILE_SHARING to 1
|
||||||
)
|
)
|
||||||
).recipientId
|
).recipientId
|
||||||
@@ -1199,6 +1200,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
SELECT ${DistributionListTables.ListTable.ID}
|
SELECT ${DistributionListTables.ListTable.ID}
|
||||||
FROM ${DistributionListTables.ListTable.TABLE_NAME}
|
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,
|
RecipientType.INDIVIDUAL.id,
|
||||||
@@ -1217,6 +1224,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
RecipientType.INDIVIDUAL -> out[id] = StorageId.forContact(key)
|
RecipientType.INDIVIDUAL -> out[id] = StorageId.forContact(key)
|
||||||
RecipientType.GV1 -> out[id] = StorageId.forGroupV1(key)
|
RecipientType.GV1 -> out[id] = StorageId.forGroupV1(key)
|
||||||
RecipientType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
|
RecipientType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
|
||||||
|
RecipientType.CALL_LINK -> out[id] = StorageId.forCallLink(key)
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3870,8 +3878,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
|
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val query = "$ID = ? AND ($TYPE IN (?, ?, ?) OR $REGISTERED = ? OR $ID = ?)"
|
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 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 ->
|
writableDatabase.update(TABLE_NAME, values, query, args).also { updateCount ->
|
||||||
Log.d(TAG, "[rotateStorageId] updateCount: $updateCount")
|
Log.d(TAG, "[rotateStorageId] updateCount: $updateCount")
|
||||||
|
|||||||
+4
-2
@@ -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.V242_MessageFullTextSearchEmojiSupportV2
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V243_MessageFullTextSearchDisableSecureDelete
|
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.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.
|
* 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,
|
241 to V241_ExpireTimerVersion,
|
||||||
242 to V242_MessageFullTextSearchEmojiSupportV2,
|
242 to V242_MessageFullTextSearchEmojiSupportV2,
|
||||||
243 to V243_MessageFullTextSearchDisableSecureDelete,
|
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
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
|||||||
+18
@@ -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(
|
.type(
|
||||||
when (callLinkUpdateType) {
|
when (callLinkUpdateType) {
|
||||||
CallLinkUpdate.Type.UPDATE -> CallLinkUpdateSendJobData.Type.UPDATE
|
CallLinkUpdate.Type.UPDATE -> CallLinkUpdateSendJobData.Type.UPDATE
|
||||||
CallLinkUpdate.Type.DELETE -> CallLinkUpdateSendJobData.Type.DELETE
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -83,10 +82,6 @@ class CallLinkUpdateSendJob private constructor(
|
|||||||
|
|
||||||
AppDependencies.signalServiceMessageSender
|
AppDependencies.signalServiceMessageSender
|
||||||
.sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate))
|
.sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate))
|
||||||
|
|
||||||
if (callLinkUpdateType == CallLinkUpdate.Type.DELETE) {
|
|
||||||
SignalDatabase.callLinks.deleteCallLink(callLinkRoomId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShouldRetry(e: Exception): Boolean {
|
override fun onShouldRetry(e: Exception): Boolean {
|
||||||
@@ -102,7 +97,6 @@ class CallLinkUpdateSendJob private constructor(
|
|||||||
val jobData = CallLinkUpdateSendJobData.ADAPTER.decode(serializedData!!)
|
val jobData = CallLinkUpdateSendJobData.ADAPTER.decode(serializedData!!)
|
||||||
val type: CallLinkUpdate.Type = when (jobData.type) {
|
val type: CallLinkUpdate.Type = when (jobData.type) {
|
||||||
CallLinkUpdateSendJobData.Type.UPDATE, null -> CallLinkUpdate.Type.UPDATE
|
CallLinkUpdateSendJobData.Type.UPDATE, null -> CallLinkUpdate.Type.UPDATE
|
||||||
CallLinkUpdateSendJobData.Type.DELETE -> CallLinkUpdate.Type.DELETE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return CallLinkUpdateSendJob(
|
return CallLinkUpdateSendJob(
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJo
|
|||||||
import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob;
|
import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.SubscriberIdMigrationJob;
|
import org.thoughtcrime.securesms.migrations.SubscriberIdMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.Svr2MirrorMigrationJob;
|
import org.thoughtcrime.securesms.migrations.Svr2MirrorMigrationJob;
|
||||||
|
import org.thoughtcrime.securesms.migrations.SyncCallLinksMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
|
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.SyncKeysMigrationJob;
|
import org.thoughtcrime.securesms.migrations.SyncKeysMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
|
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
|
||||||
@@ -299,6 +300,7 @@ public final class JobManagerFactories {
|
|||||||
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
|
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
|
||||||
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
|
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
|
||||||
put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory());
|
put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory());
|
||||||
|
put(SyncCallLinksMigrationJob.KEY, new SyncCallLinksMigrationJob.Factory());
|
||||||
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
||||||
put(SyncKeysMigrationJob.KEY, new SyncKeysMigrationJob.Factory());
|
put(SyncKeysMigrationJob.KEY, new SyncKeysMigrationJob.Factory());
|
||||||
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.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.migrations.StorageServiceMigrationJob;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.storage.AccountRecordProcessor;
|
import org.thoughtcrime.securesms.storage.AccountRecordProcessor;
|
||||||
|
import org.thoughtcrime.securesms.storage.CallLinkRecordProcessor;
|
||||||
import org.thoughtcrime.securesms.storage.ContactRecordProcessor;
|
import org.thoughtcrime.securesms.storage.ContactRecordProcessor;
|
||||||
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor;
|
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor;
|
||||||
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor;
|
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.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
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.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
@@ -287,7 +289,7 @@ public class StorageSyncJob extends BaseJob {
|
|||||||
|
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
try {
|
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);
|
processKnownRecords(context, remoteOnly);
|
||||||
|
|
||||||
@@ -411,6 +413,7 @@ public class StorageSyncJob extends BaseJob {
|
|||||||
new GroupV2RecordProcessor(context).process(records.gv2, StorageSyncHelper.KEY_GENERATOR);
|
new GroupV2RecordProcessor(context).process(records.gv2, StorageSyncHelper.KEY_GENERATOR);
|
||||||
new AccountRecordProcessor(context, freshSelf()).process(records.account, StorageSyncHelper.KEY_GENERATOR);
|
new AccountRecordProcessor(context, freshSelf()).process(records.account, StorageSyncHelper.KEY_GENERATOR);
|
||||||
new StoryDistributionListRecordProcessor().process(records.storyDistributionLists, 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) {
|
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());
|
throw new MissingRecipientModelError("Missing local recipient model! Type: " + id.getType());
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
||||||
if (unknown != null) {
|
if (unknown != null) {
|
||||||
@@ -501,6 +516,7 @@ public class StorageSyncJob extends BaseJob {
|
|||||||
final List<SignalAccountRecord> account = new LinkedList<>();
|
final List<SignalAccountRecord> account = new LinkedList<>();
|
||||||
final List<SignalStorageRecord> unknown = new LinkedList<>();
|
final List<SignalStorageRecord> unknown = new LinkedList<>();
|
||||||
final List<SignalStoryDistributionListRecord> storyDistributionLists = new LinkedList<>();
|
final List<SignalStoryDistributionListRecord> storyDistributionLists = new LinkedList<>();
|
||||||
|
final List<SignalCallLinkRecord> callLinkRecords = new LinkedList<>();
|
||||||
|
|
||||||
StorageRecordCollection(Collection<SignalStorageRecord> records) {
|
StorageRecordCollection(Collection<SignalStorageRecord> records) {
|
||||||
for (SignalStorageRecord record : records) {
|
for (SignalStorageRecord record : records) {
|
||||||
@@ -514,6 +530,8 @@ public class StorageSyncJob extends BaseJob {
|
|||||||
account.add(record.getAccount().get());
|
account.add(record.getAccount().get());
|
||||||
} else if (record.getStoryDistributionList().isPresent()) {
|
} else if (record.getStoryDistributionList().isPresent()) {
|
||||||
storyDistributionLists.add(record.getStoryDistributionList().get());
|
storyDistributionLists.add(record.getStoryDistributionList().get());
|
||||||
|
} else if (record.getCallLink().isPresent()) {
|
||||||
|
callLinkRecords.add(record.getCallLink().get());
|
||||||
} else if (record.getId().isUnknown()) {
|
} else if (record.getId().isUnknown()) {
|
||||||
unknown.add(record);
|
unknown.add(record);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1336,12 +1336,6 @@ object SyncMessageProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(callLinkRootKey)
|
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)) {
|
if (SignalDatabase.callLinks.callLinkExists(roomId)) {
|
||||||
log(envelopeTimestamp, "Synchronize call link for a link we already know about. Updating credentials.")
|
log(envelopeTimestamp, "Synchronize call link for a link we already know about. Updating credentials.")
|
||||||
@@ -1362,9 +1356,12 @@ object SyncMessageProcessor {
|
|||||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||||
adminPassBytes = callLinkUpdate.adminPassKey?.toByteArray()
|
adminPassBytes = callLinkUpdate.adminPassKey?.toByteArray()
|
||||||
),
|
),
|
||||||
state = SignalCallLinkState()
|
state = SignalCallLinkState(),
|
||||||
|
deletionTimestamp = 0L
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppDependencies.jobManager.add(RefreshCallLinkDetailsJob(callLinkUpdate))
|
AppDependencies.jobManager.add(RefreshCallLinkDetailsJob(callLinkUpdate))
|
||||||
|
|||||||
@@ -155,9 +155,10 @@ public class ApplicationMigrations {
|
|||||||
static final int EXPIRE_TIMER_CAPABILITY_2 = 111;
|
static final int EXPIRE_TIMER_CAPABILITY_2 = 111;
|
||||||
// static final int BACKFILL_DIGESTS = 112;
|
// static final int BACKFILL_DIGESTS = 112;
|
||||||
static final int BACKFILL_DIGESTS_V2 = 113;
|
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
|
* 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());
|
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;
|
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.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme;
|
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.GroupTable;
|
||||||
import org.thoughtcrime.securesms.database.IdentityTable;
|
import org.thoughtcrime.securesms.database.IdentityTable;
|
||||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
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.PhoneNumberPrivacyValues;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
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.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
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.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
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.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class StorageSyncModels {
|
public final class StorageSyncModels {
|
||||||
@@ -63,6 +67,7 @@ public final class StorageSyncModels {
|
|||||||
case GV1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
case GV1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||||
case GV2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
case GV2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
||||||
case DISTRIBUTION_LIST: return SignalStorageRecord.forStoryDistributionList(localToRemoteStoryDistributionList(settings, rawStorageId));
|
case DISTRIBUTION_LIST: return SignalStorageRecord.forStoryDistributionList(localToRemoteStoryDistributionList(settings, rawStorageId));
|
||||||
|
case CALL_LINK: return SignalStorageRecord.forCallLink(localToRemoteCallLink(settings, rawStorageId));
|
||||||
default: throw new AssertionError("Unsupported type!");
|
default: throw new AssertionError("Unsupported type!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +231,32 @@ public final class StorageSyncModels {
|
|||||||
.build();
|
.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) {
|
private static @NonNull SignalStoryDistributionListRecord localToRemoteStoryDistributionList(@NonNull RecipientRecord recipient, @NonNull byte[] rawStorageId) {
|
||||||
DistributionListId distributionListId = recipient.getDistributionListId();
|
DistributionListId distributionListId = recipient.getDistributionListId();
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ public final class StorageSyncValidations {
|
|||||||
throw new DuplicateDistributionListIdError();
|
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();
|
throw new DuplicateRawIdAcrossTypesError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +201,9 @@ public final class StorageSyncValidations {
|
|||||||
private static final class DuplicateDistributionListIdError extends Error {
|
private static final class DuplicateDistributionListIdError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class DuplicateCallLinkError extends Error {
|
||||||
|
}
|
||||||
|
|
||||||
private static final class DuplicateInsertInWriteError extends Error {
|
private static final class DuplicateInsertInWriteError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ message CallLogEventSendJobData {
|
|||||||
message CallLinkUpdateSendJobData {
|
message CallLinkUpdateSendJobData {
|
||||||
enum Type {
|
enum Type {
|
||||||
UPDATE = 0;
|
UPDATE = 0;
|
||||||
DELETE = 1;
|
reserved 1; // was DELETE, superseded by storage service
|
||||||
}
|
}
|
||||||
|
|
||||||
string callLinkRoomId = 1;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.storage
|
||||||
|
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record in storage service that represents a call link that was already created.
|
||||||
|
*/
|
||||||
|
class SignalCallLinkRecord(private val id: StorageId, private val proto: CallLinkRecord) : SignalRecord {
|
||||||
|
|
||||||
|
val rootKey: ByteArray = proto.rootKey.toByteArray()
|
||||||
|
val adminPassKey: ByteArray = proto.adminPasskey.toByteArray()
|
||||||
|
val deletionTimestamp: Long = proto.deletedAtTimestampMs
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (deletionTimestamp != 0L && adminPassKey.isNotEmpty()) {
|
||||||
|
throw IllegalStateException("Cannot have nonzero deletion timestamp ($deletionTimestamp) and admin passkey!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): CallLinkRecord {
|
||||||
|
return proto
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getId(): StorageId {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asStorageRecord(): SignalStorageRecord {
|
||||||
|
return SignalStorageRecord.forCallLink(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeDiff(other: SignalRecord?): String {
|
||||||
|
return when (other) {
|
||||||
|
is SignalCallLinkRecord -> {
|
||||||
|
val diff = LinkedList<String>()
|
||||||
|
if (!rootKey.contentEquals(other.rootKey)) {
|
||||||
|
diff.add("RootKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminPassKey.contentEquals(other.adminPassKey)) {
|
||||||
|
diff.add("AdminPassKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletionTimestamp != other.deletionTimestamp) {
|
||||||
|
diff.add("DeletionTimestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
diff.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
"Other was null!"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"Different class. ${this::class.java.getSimpleName()} | ${other::class.java.getSimpleName()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDeleted(): Boolean {
|
||||||
|
return deletionTimestamp > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder(rawId: ByteArray, serializedUnknowns: ByteArray?) {
|
||||||
|
private var id: StorageId = StorageId.forCallLink(rawId)
|
||||||
|
private var builder: CallLinkRecord.Builder
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (serializedUnknowns != null) {
|
||||||
|
this.builder = parseUnknowns(serializedUnknowns)
|
||||||
|
} else {
|
||||||
|
this.builder = CallLinkRecord.Builder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRootKey(rootKey: ByteArray): Builder {
|
||||||
|
builder.rootKey = rootKey.toByteString()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAdminPassKey(adminPasskey: ByteArray): Builder {
|
||||||
|
builder.adminPasskey = adminPasskey.toByteString()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDeletedTimestamp(deletedTimestamp: Long): Builder {
|
||||||
|
builder.deletedAtTimestampMs = deletedTimestamp
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): SignalCallLinkRecord {
|
||||||
|
return SignalCallLinkRecord(id, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseUnknowns(serializedUnknowns: ByteArray): CallLinkRecord.Builder {
|
||||||
|
return try {
|
||||||
|
CallLinkRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
CallLinkRecord.Builder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -50,7 +50,9 @@ public final class SignalStorageModels {
|
|||||||
return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.account));
|
return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.account));
|
||||||
} else if (record.storyDistributionList != null && type == ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.getValue()) {
|
} else if (record.storyDistributionList != null && type == ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.getValue()) {
|
||||||
return SignalStorageRecord.forStoryDistributionList(id, new SignalStoryDistributionListRecord(id, record.storyDistributionList));
|
return SignalStorageRecord.forStoryDistributionList(id, new SignalStoryDistributionListRecord(id, record.storyDistributionList));
|
||||||
} else {
|
} else if (record.callLink != null && type == ManifestRecord.Identifier.Type.CALL_LINK.getValue()) {
|
||||||
|
return SignalStorageRecord.forCallLink(id, new SignalCallLinkRecord(id, record.callLink));
|
||||||
|
}else {
|
||||||
if (StorageId.isKnownType(type)) {
|
if (StorageId.isKnownType(type)) {
|
||||||
Log.w(TAG, "StorageId is of known type (" + type + "), but the data is bad! Falling back to unknown.");
|
Log.w(TAG, "StorageId is of known type (" + type + "), but the data is bad! Falling back to unknown.");
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,8 @@ public final class SignalStorageModels {
|
|||||||
builder.account(record.getAccount().get().toProto());
|
builder.account(record.getAccount().get().toProto());
|
||||||
} else if (record.getStoryDistributionList().isPresent()) {
|
} else if (record.getStoryDistributionList().isPresent()) {
|
||||||
builder.storyDistributionList(record.getStoryDistributionList().get().toProto());
|
builder.storyDistributionList(record.getStoryDistributionList().get().toProto());
|
||||||
|
} else if (record.getCallLink().isPresent()) {
|
||||||
|
builder.callLink(record.getCallLink().get().toProto());
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidStorageWriteError();
|
throw new InvalidStorageWriteError();
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-10
@@ -1,6 +1,9 @@
|
|||||||
package org.whispersystems.signalservice.api.storage;
|
package org.whispersystems.signalservice.api.storage;
|
||||||
|
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -12,13 +15,14 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
private final Optional<SignalGroupV1Record> groupV1;
|
private final Optional<SignalGroupV1Record> groupV1;
|
||||||
private final Optional<SignalGroupV2Record> groupV2;
|
private final Optional<SignalGroupV2Record> groupV2;
|
||||||
private final Optional<SignalAccountRecord> account;
|
private final Optional<SignalAccountRecord> account;
|
||||||
|
private final Optional<SignalCallLinkRecord> callLink;
|
||||||
|
|
||||||
public static SignalStorageRecord forStoryDistributionList(SignalStoryDistributionListRecord storyDistributionList) {
|
public static SignalStorageRecord forStoryDistributionList(SignalStoryDistributionListRecord storyDistributionList) {
|
||||||
return forStoryDistributionList(storyDistributionList.getId(), storyDistributionList);
|
return forStoryDistributionList(storyDistributionList.getId(), storyDistributionList);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forStoryDistributionList(StorageId key, SignalStoryDistributionListRecord storyDistributionList) {
|
public static SignalStorageRecord forStoryDistributionList(StorageId key, SignalStoryDistributionListRecord storyDistributionList) {
|
||||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(storyDistributionList));
|
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(storyDistributionList), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
||||||
@@ -26,7 +30,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) {
|
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) {
|
||||||
return new SignalStorageRecord(key, Optional.of(contact), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
return new SignalStorageRecord(key, Optional.of(contact), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
||||||
@@ -34,7 +38,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
|
public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
|
||||||
return new SignalStorageRecord(key, Optional.empty(), Optional.of(groupV1), Optional.empty(), Optional.empty(), Optional.empty());
|
return new SignalStorageRecord(key, Optional.empty(), Optional.of(groupV1), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) {
|
public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) {
|
||||||
@@ -42,7 +46,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) {
|
public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) {
|
||||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.of(groupV2), Optional.empty(), Optional.empty());
|
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.of(groupV2), Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forAccount(SignalAccountRecord account) {
|
public static SignalStorageRecord forAccount(SignalAccountRecord account) {
|
||||||
@@ -50,19 +54,31 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) {
|
public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) {
|
||||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(account), Optional.empty());
|
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(account), Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static SignalStorageRecord forCallLink(@NotNull SignalCallLinkRecord callLink) {
|
||||||
|
return forCallLink(callLink.getId(), callLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static SignalStorageRecord forCallLink(StorageId key, @NotNull SignalCallLinkRecord callLink) {
|
||||||
|
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(callLink));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forUnknown(StorageId key) {
|
public static SignalStorageRecord forUnknown(StorageId key) {
|
||||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private SignalStorageRecord(StorageId id,
|
private SignalStorageRecord(StorageId id,
|
||||||
Optional<SignalContactRecord> contact,
|
Optional<SignalContactRecord> contact,
|
||||||
Optional<SignalGroupV1Record> groupV1,
|
Optional<SignalGroupV1Record> groupV1,
|
||||||
Optional<SignalGroupV2Record> groupV2,
|
Optional<SignalGroupV2Record> groupV2,
|
||||||
Optional<SignalAccountRecord> account,
|
Optional<SignalAccountRecord> account,
|
||||||
Optional<SignalStoryDistributionListRecord> storyDistributionList)
|
Optional<SignalStoryDistributionListRecord> storyDistributionList,
|
||||||
|
Optional<SignalCallLinkRecord> callLink)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.contact = contact;
|
this.contact = contact;
|
||||||
@@ -70,6 +86,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
this.groupV2 = groupV2;
|
this.groupV2 = groupV2;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.storyDistributionList = storyDistributionList;
|
this.storyDistributionList = storyDistributionList;
|
||||||
|
this.callLink = callLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -111,8 +128,12 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
return storyDistributionList;
|
return storyDistributionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<SignalCallLinkRecord> getCallLink() {
|
||||||
|
return callLink;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUnknown() {
|
public boolean isUnknown() {
|
||||||
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent() && !storyDistributionList.isPresent();
|
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent() && !storyDistributionList.isPresent() && !callLink.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -124,11 +145,12 @@ public class SignalStorageRecord implements SignalRecord {
|
|||||||
Objects.equals(contact, that.contact) &&
|
Objects.equals(contact, that.contact) &&
|
||||||
Objects.equals(groupV1, that.groupV1) &&
|
Objects.equals(groupV1, that.groupV1) &&
|
||||||
Objects.equals(groupV2, that.groupV2) &&
|
Objects.equals(groupV2, that.groupV2) &&
|
||||||
Objects.equals(storyDistributionList, that.storyDistributionList);
|
Objects.equals(storyDistributionList, that.storyDistributionList) &&
|
||||||
|
Objects.equals(callLink, that.callLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(id, contact, groupV1, groupV2, storyDistributionList);
|
return Objects.hash(id, contact, groupV1, groupV2, storyDistributionList, callLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -31,6 +31,10 @@ public class StorageId {
|
|||||||
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT.getValue(), Preconditions.checkNotNull(raw));
|
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT.getValue(), Preconditions.checkNotNull(raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static StorageId forCallLink(byte[] raw) {
|
||||||
|
return new StorageId(ManifestRecord.Identifier.Type.CALL_LINK.getValue(), Preconditions.checkNotNull(raw));
|
||||||
|
}
|
||||||
|
|
||||||
public static StorageId forType(byte[] raw, int type) {
|
public static StorageId forType(byte[] raw, int type) {
|
||||||
return new StorageId(type, raw);
|
return new StorageId(type, raw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ message SyncMessage {
|
|||||||
message CallLinkUpdate {
|
message CallLinkUpdate {
|
||||||
enum Type {
|
enum Type {
|
||||||
UPDATE = 0;
|
UPDATE = 0;
|
||||||
DELETE = 1;
|
reserved 1; // was DELETE, superseded by storage service
|
||||||
}
|
}
|
||||||
|
|
||||||
optional bytes rootKey = 1;
|
optional bytes rootKey = 1;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ message ManifestRecord {
|
|||||||
GROUPV2 = 3;
|
GROUPV2 = 3;
|
||||||
ACCOUNT = 4;
|
ACCOUNT = 4;
|
||||||
STORY_DISTRIBUTION_LIST = 5;
|
STORY_DISTRIBUTION_LIST = 5;
|
||||||
|
CALL_LINK = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes raw = 1;
|
bytes raw = 1;
|
||||||
@@ -69,6 +70,7 @@ message StorageRecord {
|
|||||||
GroupV2Record groupV2 = 3;
|
GroupV2Record groupV2 = 3;
|
||||||
AccountRecord account = 4;
|
AccountRecord account = 4;
|
||||||
StoryDistributionListRecord storyDistributionList = 5;
|
StoryDistributionListRecord storyDistributionList = 5;
|
||||||
|
CallLinkRecord callLink = 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,3 +229,9 @@ message StoryDistributionListRecord {
|
|||||||
bool allowsReplies = 5;
|
bool allowsReplies = 5;
|
||||||
bool isBlockList = 6;
|
bool isBlockList = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CallLinkRecord {
|
||||||
|
bytes rootKey = 1;
|
||||||
|
bytes adminPasskey = 2;
|
||||||
|
uint64 deletedAtTimestampMs = 3;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user