mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Improve implementation and testing on PNP contact merging.
This commit is contained in:
committed by
Cody Henthorne
parent
c64be82710
commit
61ce39b5b6
@@ -0,0 +1,215 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import app.cash.exhaustive.Exhaustive
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
|
||||
/**
|
||||
* Encapsulates data around processing a tuple of user data into a user entry in [RecipientDatabase].
|
||||
* Also lets you apply a list of [PnpOperation]s to get what the resulting dataset would be.
|
||||
*/
|
||||
data class PnpDataSet(
|
||||
val e164: String?,
|
||||
val pni: PNI?,
|
||||
val aci: ACI?,
|
||||
val byE164: RecipientId?,
|
||||
val byPniSid: RecipientId?,
|
||||
val byPniOnly: RecipientId?,
|
||||
val byAciSid: RecipientId?,
|
||||
val e164Record: RecipientRecord? = null,
|
||||
val pniSidRecord: RecipientRecord? = null,
|
||||
val aciSidRecord: RecipientRecord? = null
|
||||
) {
|
||||
|
||||
/**
|
||||
* @return The common id if all non-null ids are equal, or null if all are null or at least one non-null pair doesn't match.
|
||||
*/
|
||||
val commonId: RecipientId? = findCommonId(listOf(byE164, byPniSid, byPniOnly, byAciSid))
|
||||
|
||||
fun MutableSet<RecipientRecord>.replace(recipientId: RecipientId, update: (RecipientRecord) -> RecipientRecord) {
|
||||
val toUpdate = this.first { it.id == recipientId }
|
||||
this -= toUpdate
|
||||
this += update(toUpdate)
|
||||
}
|
||||
/**
|
||||
* Applies the set of operations and returns the resulting dataset.
|
||||
* Important: This only occurs _in memory_. You must still apply the operations to disk to persist them.
|
||||
*/
|
||||
fun perform(operations: List<PnpOperation>): PnpDataSet {
|
||||
if (operations.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val records: MutableSet<RecipientRecord> = listOfNotNull(e164Record, pniSidRecord, aciSidRecord).toMutableSet()
|
||||
|
||||
for (operation in operations) {
|
||||
@Exhaustive
|
||||
when (operation) {
|
||||
is PnpOperation.Update -> {
|
||||
records.replace(operation.recipientId) { record ->
|
||||
record.copy(
|
||||
e164 = operation.e164,
|
||||
pni = operation.pni,
|
||||
serviceId = operation.aci ?: operation.pni
|
||||
)
|
||||
}
|
||||
}
|
||||
is PnpOperation.RemoveE164 -> {
|
||||
records.replace(operation.recipientId) { it.copy(e164 = null) }
|
||||
}
|
||||
is PnpOperation.RemovePni -> {
|
||||
records.replace(operation.recipientId) { record ->
|
||||
record.copy(
|
||||
pni = null,
|
||||
serviceId = if (record.sidIsPni()) {
|
||||
null
|
||||
} else {
|
||||
record.serviceId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is PnpOperation.SetAci -> {
|
||||
records.replace(operation.recipientId) { it.copy(serviceId = operation.aci) }
|
||||
}
|
||||
is PnpOperation.SetE164 -> {
|
||||
records.replace(operation.recipientId) { it.copy(e164 = operation.e164) }
|
||||
}
|
||||
is PnpOperation.SetPni -> {
|
||||
records.replace(operation.recipientId) { record ->
|
||||
record.copy(
|
||||
pni = operation.pni,
|
||||
serviceId = if (record.sidIsPni()) {
|
||||
operation.pni
|
||||
} else {
|
||||
record.serviceId ?: operation.pni
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is PnpOperation.Merge -> {
|
||||
val primary: RecipientRecord = records.first { it.id == operation.primaryId }
|
||||
val secondary: RecipientRecord = records.first { it.id == operation.secondaryId }
|
||||
|
||||
records.replace(primary.id) { _ ->
|
||||
primary.copy(
|
||||
e164 = primary.e164 ?: secondary.e164,
|
||||
pni = primary.pni ?: secondary.pni,
|
||||
serviceId = primary.serviceId ?: secondary.serviceId
|
||||
)
|
||||
}
|
||||
|
||||
records -= secondary
|
||||
}
|
||||
is PnpOperation.SessionSwitchoverInsert -> Unit
|
||||
is PnpOperation.ChangeNumberInsert -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
val newE164Record = if (e164 != null) records.firstOrNull { it.e164 == e164 } else null
|
||||
val newPniSidRecord = if (pni != null) records.firstOrNull { it.serviceId == pni } else null
|
||||
val newAciSidRecord = if (aci != null) records.firstOrNull { it.serviceId == aci } else null
|
||||
|
||||
return PnpDataSet(
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
aci = aci,
|
||||
byE164 = newE164Record?.id,
|
||||
byPniSid = newPniSidRecord?.id,
|
||||
byPniOnly = byPniOnly,
|
||||
byAciSid = newAciSidRecord?.id,
|
||||
e164Record = newE164Record,
|
||||
pniSidRecord = newPniSidRecord,
|
||||
aciSidRecord = newAciSidRecord
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun findCommonId(ids: List<RecipientId?>): RecipientId? {
|
||||
val nonNull = ids.filterNotNull()
|
||||
|
||||
return when {
|
||||
nonNull.isEmpty() -> null
|
||||
nonNull.all { it == nonNull[0] } -> nonNull[0]
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a set of actions that need to be applied to incorporate a tuple of user data
|
||||
* into [RecipientDatabase].
|
||||
*/
|
||||
data class PnpChangeSet(
|
||||
val id: PnpIdResolver,
|
||||
val operations: List<PnpOperation> = emptyList()
|
||||
)
|
||||
|
||||
sealed class PnpIdResolver {
|
||||
data class PnpNoopId(
|
||||
val recipientId: RecipientId
|
||||
) : PnpIdResolver()
|
||||
|
||||
data class PnpInsert(
|
||||
val e164: String?,
|
||||
val pni: PNI?,
|
||||
val aci: ACI?
|
||||
) : PnpIdResolver()
|
||||
}
|
||||
|
||||
/**
|
||||
* An operation that needs to be performed on the [RecipientDatabase] as part of merging in new user data.
|
||||
* Lets us describe various situations as a series of operations, making code clearer and tests easier.
|
||||
*/
|
||||
sealed class PnpOperation {
|
||||
data class Update(
|
||||
val recipientId: RecipientId,
|
||||
val e164: String?,
|
||||
val pni: PNI?,
|
||||
val aci: ACI?
|
||||
) : PnpOperation()
|
||||
|
||||
data class RemoveE164(
|
||||
val recipientId: RecipientId
|
||||
) : PnpOperation()
|
||||
|
||||
data class RemovePni(
|
||||
val recipientId: RecipientId
|
||||
) : PnpOperation()
|
||||
|
||||
data class SetE164(
|
||||
val recipientId: RecipientId,
|
||||
val e164: String
|
||||
) : PnpOperation()
|
||||
|
||||
data class SetPni(
|
||||
val recipientId: RecipientId,
|
||||
val pni: PNI
|
||||
) : PnpOperation()
|
||||
|
||||
data class SetAci(
|
||||
val recipientId: RecipientId,
|
||||
val aci: ACI
|
||||
) : PnpOperation()
|
||||
|
||||
/**
|
||||
* Merge two rows into one. Prefer data in the primary row when there's conflicts. Delete the secondary row afterwards.
|
||||
*/
|
||||
data class Merge(
|
||||
val primaryId: RecipientId,
|
||||
val secondaryId: RecipientId
|
||||
) : PnpOperation()
|
||||
|
||||
data class SessionSwitchoverInsert(
|
||||
val recipientId: RecipientId
|
||||
) : PnpOperation()
|
||||
|
||||
data class ChangeNumberInsert(
|
||||
val recipientId: RecipientId,
|
||||
val oldE164: String,
|
||||
val newE164: String
|
||||
) : PnpOperation()
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.contentValuesOf
|
||||
import app.cash.exhaustive.Exhaustive
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import net.zetetic.database.sqlcipher.SQLiteConstraintException
|
||||
@@ -54,7 +55,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notificationProfiles
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.storySends
|
||||
@@ -105,6 +105,7 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.util.Arrays
|
||||
@@ -2179,43 +2180,388 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
@VisibleForTesting
|
||||
fun processCdsV2Result(e164: String, pni: PNI, aci: ACI?): RecipientId {
|
||||
val byE164: RecipientId? = getByE164(e164).orElse(null)
|
||||
val byPni: RecipientId? = getByServiceId(pni).orElse(null)
|
||||
val byPniOnly: RecipientId? = getByPni(pni).orElse(null)
|
||||
val byAci: RecipientId? = aci?.let { getByServiceId(it).orElse(null) }
|
||||
val result = processPnpTupleToChangeSet(e164, pni, aci, pniVerified = false)
|
||||
|
||||
val commonId: RecipientId? = listOf(byE164, byPni, byPniOnly, byAci).commonId()
|
||||
val allRequiredDbFields: List<RecipientId?> = if (aci != null) listOf(byE164, byAci, byPniOnly) else listOf(byE164, byPni, byPniOnly)
|
||||
val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
|
||||
|
||||
// All ID's agree and the database is up-to-date
|
||||
if (commonId != null && allRequiredDbFieldPopulated) {
|
||||
return commonId
|
||||
val id: RecipientId = when (result.id) {
|
||||
is PnpIdResolver.PnpNoopId -> {
|
||||
result.id.recipientId
|
||||
}
|
||||
is PnpIdResolver.PnpInsert -> {
|
||||
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForCdsInsert(result.id.e164, result.id.pni, result.id.aci))
|
||||
RecipientId.from(id)
|
||||
}
|
||||
}
|
||||
|
||||
// All ID's agree but we need to update the database
|
||||
if (commonId != null && !allRequiredDbFieldPopulated) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
PHONE to e164,
|
||||
SERVICE_ID to (aci ?: pni).toString(),
|
||||
PNI_COLUMN to pni.toString(),
|
||||
REGISTERED to RegisteredState.REGISTERED.id,
|
||||
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey())
|
||||
for (operation in result.operations) {
|
||||
@Exhaustive
|
||||
when (operation) {
|
||||
is PnpOperation.Update -> {
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(
|
||||
PHONE to operation.e164,
|
||||
SERVICE_ID to (operation.aci ?: operation.pni).toString(),
|
||||
PNI_COLUMN to operation.pni.toString()
|
||||
)
|
||||
.where("$ID = ?", operation.recipientId)
|
||||
.run()
|
||||
}
|
||||
is PnpOperation.Merge -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.SessionSwitchoverInsert -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.ChangeNumberInsert -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.RemoveE164 -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.RemovePni -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.SetAci -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.SetE164 -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
is PnpOperation.SetPni -> {
|
||||
// TODO [pnp]
|
||||
error("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a tuple of (e164, pni, aci) and converts that into a list of changes that would need to be made to
|
||||
* merge that data into our database.
|
||||
*
|
||||
* The database will be read, but not written to, during this function.
|
||||
* It is assumed that we are in a transaction.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun processPnpTupleToChangeSet(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean): PnpChangeSet {
|
||||
Preconditions.checkArgument(e164 != null || pni != null || aci != null, "Must provide at least one field!")
|
||||
Preconditions.checkArgument(pni == null || e164 != null, "If a PNI is provided, you must also provide an E164!")
|
||||
|
||||
val partialData = PnpDataSet(
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
aci = aci,
|
||||
byE164 = e164?.let { getByE164(it).orElse(null) },
|
||||
byPniSid = pni?.let { getByServiceId(it).orElse(null) },
|
||||
byPniOnly = pni?.let { getByPni(it).orElse(null) },
|
||||
byAciSid = aci?.let { getByServiceId(it).orElse(null) }
|
||||
)
|
||||
|
||||
val allRequiredDbFields: List<RecipientId?> = if (aci != null) {
|
||||
listOf(partialData.byE164, partialData.byAciSid, partialData.byPniOnly)
|
||||
} else {
|
||||
listOf(partialData.byE164, partialData.byPniSid, partialData.byPniOnly)
|
||||
}
|
||||
|
||||
val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
|
||||
|
||||
// All IDs agree and the database is up-to-date
|
||||
if (partialData.commonId != null && allRequiredDbFieldPopulated) {
|
||||
return PnpChangeSet(id = PnpIdResolver.PnpNoopId(partialData.commonId))
|
||||
}
|
||||
|
||||
// All ID's agree, but we need to update the database
|
||||
if (partialData.commonId != null && !allRequiredDbFieldPopulated) {
|
||||
val record: RecipientRecord = getRecord(partialData.commonId)
|
||||
|
||||
val operations: MutableList<PnpOperation> = mutableListOf()
|
||||
|
||||
if (e164 != null && record.e164 != e164) {
|
||||
operations += PnpOperation.SetE164(
|
||||
recipientId = partialData.commonId,
|
||||
e164 = e164
|
||||
)
|
||||
.where("$ID = ?", commonId)
|
||||
.run()
|
||||
return commonId
|
||||
}
|
||||
|
||||
if (pni != null && record.pni != pni) {
|
||||
operations += PnpOperation.SetPni(
|
||||
recipientId = partialData.commonId,
|
||||
pni = pni
|
||||
)
|
||||
}
|
||||
|
||||
if (aci != null && record.serviceId != aci) {
|
||||
operations += PnpOperation.SetAci(
|
||||
recipientId = partialData.commonId,
|
||||
aci = aci
|
||||
)
|
||||
}
|
||||
|
||||
if (e164 != null && record.e164 != null && record.e164 != e164) {
|
||||
operations += PnpOperation.ChangeNumberInsert(
|
||||
recipientId = partialData.commonId,
|
||||
oldE164 = record.e164,
|
||||
newE164 = e164
|
||||
)
|
||||
}
|
||||
|
||||
val newServiceId: ServiceId? = aci ?: pni ?: record.serviceId
|
||||
|
||||
if (!pniVerified && record.serviceId != null && record.serviceId != newServiceId && sessions.hasAnySessionFor(record.serviceId.toString())) {
|
||||
operations += PnpOperation.SessionSwitchoverInsert(partialData.commonId)
|
||||
}
|
||||
|
||||
return PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(partialData.commonId),
|
||||
operations = operations
|
||||
)
|
||||
}
|
||||
|
||||
// Nothing matches
|
||||
if (byE164 == null && byPni == null && byAci == null) {
|
||||
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForCdsInsert(e164, pni, aci))
|
||||
return RecipientId.from(id)
|
||||
if (partialData.byE164 == null && partialData.byPniSid == null && partialData.byAciSid == null) {
|
||||
return PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
aci = aci
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
throw NotImplementedError("Handle cases where IDs map to different individuals")
|
||||
// TODO pni only record?
|
||||
|
||||
// At this point, we know that records have been found for at least two of the fields,
|
||||
// and that there are at least two unique IDs among the records.
|
||||
//
|
||||
// In other words, *some* sort of merging of data must now occur.
|
||||
// It may be that some data just gets shuffled around, or it may be that
|
||||
// two or more records get merged into one record, with the others being deleted.
|
||||
|
||||
val fullData = partialData.copy(
|
||||
e164Record = partialData.byE164?.let { getRecord(it) },
|
||||
pniSidRecord = partialData.byPniSid?.let { getRecord(it) },
|
||||
aciSidRecord = partialData.byAciSid?.let { getRecord(it) },
|
||||
)
|
||||
|
||||
Preconditions.checkState(fullData.commonId == null)
|
||||
Preconditions.checkState(listOfNotNull(fullData.byE164, fullData.byPniSid, fullData.byPniOnly, fullData.byAciSid).size >= 2)
|
||||
|
||||
val operations: MutableList<PnpOperation> = mutableListOf()
|
||||
|
||||
operations += processPossibleE164PniSidMerge(pni, pniVerified, fullData)
|
||||
operations += processPossiblePniSidAciSidMerge(e164, pni, aci, fullData.perform(operations))
|
||||
operations += processPossibleE164AciSidMerge(e164, pni, aci, fullData.perform(operations))
|
||||
|
||||
val finalData: PnpDataSet = fullData.perform(operations)
|
||||
val primaryId: RecipientId = listOfNotNull(finalData.byAciSid, finalData.byE164, finalData.byPniSid).first()
|
||||
|
||||
if (finalData.byAciSid == null && aci != null) {
|
||||
operations += PnpOperation.SetAci(
|
||||
recipientId = primaryId,
|
||||
aci = aci
|
||||
)
|
||||
}
|
||||
|
||||
if (finalData.byE164 == null && e164 != null) {
|
||||
operations += PnpOperation.SetE164(
|
||||
recipientId = primaryId,
|
||||
e164 = e164
|
||||
)
|
||||
}
|
||||
|
||||
if (finalData.byPniSid == null && finalData.byPniOnly == null && pni != null) {
|
||||
operations += PnpOperation.SetPni(
|
||||
recipientId = primaryId,
|
||||
pni = pni
|
||||
)
|
||||
}
|
||||
|
||||
return PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(primaryId),
|
||||
operations = operations
|
||||
)
|
||||
}
|
||||
|
||||
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet): List<PnpOperation> {
|
||||
if (pni == null || data.byE164 == null || data.byPniSid == null || data.e164Record == null || data.pniSidRecord == null || data.e164Record.id == data.pniSidRecord.id) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// We have found records for both the E164 and PNI, and they're different
|
||||
|
||||
val operations: MutableList<PnpOperation> = mutableListOf()
|
||||
|
||||
// The PNI record only has a single identifier. We know we must merge.
|
||||
if (data.pniSidRecord.sidOnly(pni)) {
|
||||
if (data.e164Record.pni != null) {
|
||||
operations += PnpOperation.RemovePni(data.byE164)
|
||||
}
|
||||
|
||||
operations += PnpOperation.Merge(
|
||||
primaryId = data.byE164,
|
||||
secondaryId = data.byPniSid
|
||||
)
|
||||
|
||||
// TODO: Possible session switchover?
|
||||
} else {
|
||||
Preconditions.checkState(!data.pniSidRecord.pniAndAci())
|
||||
Preconditions.checkState(data.pniSidRecord.e164 != null)
|
||||
|
||||
operations += PnpOperation.RemovePni(data.byPniSid)
|
||||
operations += PnpOperation.SetPni(
|
||||
recipientId = data.byE164,
|
||||
pni = pni
|
||||
)
|
||||
|
||||
if (!pniVerified && sessions.hasAnySessionFor(data.pniSidRecord.serviceId.toString())) {
|
||||
operations += PnpOperation.SessionSwitchoverInsert(data.byPniSid)
|
||||
}
|
||||
|
||||
if (!pniVerified && data.e164Record.serviceId != null && data.e164Record.sidIsPni() && sessions.hasAnySessionFor(data.e164Record.serviceId.toString())) {
|
||||
operations += PnpOperation.SessionSwitchoverInsert(data.byE164)
|
||||
}
|
||||
}
|
||||
|
||||
return operations
|
||||
}
|
||||
|
||||
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): List<PnpOperation> {
|
||||
if (pni == null || aci == null || data.byPniSid == null || data.byAciSid == null || data.pniSidRecord == null || data.aciSidRecord == null || data.pniSidRecord.id == data.aciSidRecord.id) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// We have found records for both the PNI and ACI, and they're different
|
||||
|
||||
val operations: MutableList<PnpOperation> = mutableListOf()
|
||||
|
||||
// The PNI record only has a single identifier. We know we must merge.
|
||||
if (data.pniSidRecord.sidOnly(pni)) {
|
||||
if (data.aciSidRecord.pni != null) {
|
||||
operations += PnpOperation.RemovePni(data.byAciSid)
|
||||
}
|
||||
|
||||
operations += PnpOperation.Merge(
|
||||
primaryId = data.byAciSid,
|
||||
secondaryId = data.byPniSid
|
||||
)
|
||||
} else if (data.pniSidRecord.e164 == e164) {
|
||||
// The PNI record also has the E164 on it. We're going to be stealing both fields,
|
||||
// so this is basically a merge with a little bit of extra prep.
|
||||
|
||||
if (data.aciSidRecord.pni != null) {
|
||||
operations += PnpOperation.RemovePni(data.byAciSid)
|
||||
}
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.RemoveE164(data.byAciSid)
|
||||
}
|
||||
|
||||
operations += PnpOperation.Merge(
|
||||
primaryId = data.byAciSid,
|
||||
secondaryId = data.byPniSid
|
||||
)
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.ChangeNumberInsert(
|
||||
recipientId = data.byAciSid,
|
||||
oldE164 = data.aciSidRecord.e164,
|
||||
newE164 = e164!!
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Preconditions.checkState(data.pniSidRecord.e164 != null && data.pniSidRecord.e164 != e164)
|
||||
|
||||
operations += PnpOperation.RemovePni(data.byPniSid)
|
||||
|
||||
operations += PnpOperation.Update(
|
||||
recipientId = data.byAciSid,
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
aci = ACI.from(data.aciSidRecord.serviceId)
|
||||
)
|
||||
}
|
||||
|
||||
return operations
|
||||
}
|
||||
|
||||
private fun processPossibleE164AciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): List<PnpOperation> {
|
||||
if (e164 == null || aci == null || data.byE164 == null || data.byAciSid == null || data.e164Record == null || data.aciSidRecord == null || data.e164Record.id == data.aciSidRecord.id) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// We have found records for both the E164 and ACI, and they're different
|
||||
|
||||
val operations: MutableList<PnpOperation> = mutableListOf()
|
||||
|
||||
// The PNI record only has a single identifier. We know we must merge.
|
||||
if (data.e164Record.e164Only()) {
|
||||
// TODO high trust
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.RemoveE164(data.byAciSid)
|
||||
}
|
||||
|
||||
operations += PnpOperation.Merge(
|
||||
primaryId = data.byAciSid,
|
||||
secondaryId = data.byE164
|
||||
)
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.ChangeNumberInsert(
|
||||
recipientId = data.byAciSid,
|
||||
oldE164 = data.aciSidRecord.e164,
|
||||
newE164 = e164
|
||||
)
|
||||
}
|
||||
} else if (data.e164Record.pni != null && data.e164Record.pni == pni) {
|
||||
// The E164 record also has the PNI on it. We're going to be stealing both fields,
|
||||
// so this is basically a merge with a little bit of extra prep.
|
||||
if (data.aciSidRecord.pni != null) {
|
||||
operations += PnpOperation.RemovePni(data.byAciSid)
|
||||
}
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.RemoveE164(data.byAciSid)
|
||||
}
|
||||
|
||||
operations += PnpOperation.Merge(
|
||||
primaryId = data.byAciSid,
|
||||
secondaryId = data.byE164
|
||||
)
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.ChangeNumberInsert(
|
||||
recipientId = data.byAciSid,
|
||||
oldE164 = data.aciSidRecord.e164,
|
||||
newE164 = e164!!
|
||||
)
|
||||
}
|
||||
} else {
|
||||
operations += PnpOperation.RemoveE164(data.byE164)
|
||||
|
||||
operations += PnpOperation.SetE164(
|
||||
recipientId = data.byAciSid,
|
||||
e164 = e164
|
||||
)
|
||||
|
||||
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
|
||||
operations += PnpOperation.ChangeNumberInsert(
|
||||
recipientId = data.byAciSid,
|
||||
oldE164 = data.aciSidRecord.e164,
|
||||
newE164 = e164
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return operations
|
||||
}
|
||||
|
||||
fun getUninvitedRecipientsForInsights(): List<RecipientId> {
|
||||
@@ -2939,16 +3285,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
return values
|
||||
}
|
||||
|
||||
private fun buildContentValuesForCdsInsert(e164: String, pni: PNI, aci: ACI?): ContentValues {
|
||||
val serviceId: ServiceId = aci ?: pni
|
||||
return ContentValues().apply {
|
||||
put(PHONE, e164)
|
||||
put(SERVICE_ID, serviceId.toString())
|
||||
put(PNI_COLUMN, pni.toString())
|
||||
put(REGISTERED, RegisteredState.REGISTERED.id)
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
|
||||
put(AVATAR_COLOR, AvatarColor.random().serialize())
|
||||
}
|
||||
private fun buildContentValuesForCdsInsert(e164: String?, pni: PNI?, aci: ACI?): ContentValues {
|
||||
Preconditions.checkArgument(pni != null || aci != null, "Must provide a serviceId!")
|
||||
|
||||
val serviceId: ServiceId = aci ?: pni!!
|
||||
return contentValuesOf(
|
||||
PHONE to e164,
|
||||
SERVICE_ID to serviceId.toString(),
|
||||
PNI_COLUMN to pni.toString(),
|
||||
REGISTERED to RegisteredState.REGISTERED.id,
|
||||
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||
AVATAR_COLOR to AvatarColor.random().serialize()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
|
||||
@@ -3026,22 +3374,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The common id if all non-null ids are equal, or null if all are null or at least one non-null pair doesn't match.
|
||||
*/
|
||||
private fun Collection<RecipientId?>.commonId(): RecipientId? {
|
||||
val nonNull = this.filterNotNull()
|
||||
if (nonNull.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (nonNull.all { it.equals(nonNull[0]) }) {
|
||||
nonNull[0]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
@@ -195,5 +196,19 @@ class SessionDatabase(context: Context, databaseHelper: SignalDatabase) : Databa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if a session exists with this address for _any_ of your identities.
|
||||
*/
|
||||
fun hasAnySessionFor(addressName: String): Boolean {
|
||||
readableDatabase
|
||||
.select("1")
|
||||
.from(TABLE_NAME)
|
||||
.where("$ADDRESS = ?", addressName)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
class SessionRow(val address: String, val deviceId: Int, val record: SessionRecord)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,22 @@ data class RecipientRecord(
|
||||
return if (defaultSubscriptionId != -1) Optional.of(defaultSubscriptionId) else Optional.empty()
|
||||
}
|
||||
|
||||
fun e164Only(): Boolean {
|
||||
return this.e164 != null && this.serviceId == null
|
||||
}
|
||||
|
||||
fun sidOnly(sid: ServiceId): Boolean {
|
||||
return this.e164 == null && this.serviceId == sid && (this.pni == null || this.pni == sid)
|
||||
}
|
||||
|
||||
fun sidIsPni(): Boolean {
|
||||
return this.serviceId != null && this.pni != null && this.serviceId == this.pni
|
||||
}
|
||||
|
||||
fun pniAndAci(): Boolean {
|
||||
return this.serviceId != null && this.pni != null && this.serviceId != this.pni
|
||||
}
|
||||
|
||||
/**
|
||||
* A bundle of data that's only necessary when syncing to storage service, not for a
|
||||
* [Recipient].
|
||||
|
||||
Reference in New Issue
Block a user