Signal-Android/app/src/main/java/org/thoughtcrime/securesms/database/PnpOperations.kt

225 wiersze
6.6 KiB
Kotlin

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.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(),
val breadCrumbs: List<String> = emptyList()
) {
// We want to exclude breadcrumbs from equality for testing purposes
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PnpChangeSet
if (id != other.id) return false
if (operations != other.operations) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + operations.hashCode()
return result
}
}
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 {
abstract val recipientId: RecipientId
data class RemoveE164(
override val recipientId: RecipientId
) : PnpOperation()
data class RemovePni(
override val recipientId: RecipientId
) : PnpOperation()
data class SetE164(
override val recipientId: RecipientId,
val e164: String
) : PnpOperation()
data class SetPni(
override val recipientId: RecipientId,
val pni: PNI
) : PnpOperation()
data class SetAci(
override 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() {
override val recipientId: RecipientId
get() = throw UnsupportedOperationException()
}
data class SessionSwitchoverInsert(
override val recipientId: RecipientId
) : PnpOperation()
data class ChangeNumberInsert(
override val recipientId: RecipientId,
val oldE164: String,
val newE164: String
) : PnpOperation()
}