Improve implementation and testing on PNP contact merging.
This commit is contained in:
parent
c64be82710
commit
61ce39b5b6
8 changed files with 1455 additions and 74 deletions
|
@ -80,25 +80,6 @@ class DistributionListDatabaseTest {
|
|||
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
distributionDatabase.setAllowsReplies(id!!, false)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertFalse(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertTrue(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
|
||||
distributionDatabase.getStoryType(DistributionListId.from(12))
|
||||
|
|
|
@ -0,0 +1,810 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.lang.AssertionError
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
private lateinit var db: RecipientDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.recipients
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164Only() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, null, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_e164AndPni() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_aciOnly() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_pniOnly() {
|
||||
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun noMatch_noData() {
|
||||
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noMatch_allFields() {
|
||||
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
|
||||
),
|
||||
changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullMatch() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun e164AndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_A, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_noExistingSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyPniMatches_existingPniSession_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetAci(result.id, ACI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
),
|
||||
PnpOperation.SessionSwitchoverInsert(result.id)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pniAndAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, PNI_A, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches() {
|
||||
val result = applyAndAssert(
|
||||
Input(null, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyAciMatches_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
Input(E164_B, null, ACI_A),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.id),
|
||||
operations = listOf(
|
||||
PnpOperation.SetE164(result.id, E164_A),
|
||||
PnpOperation.SetPni(result.id, PNI_A),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.id,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
Input(null, null, ACI_A)
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.thirdId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.thirdId,
|
||||
secondaryId = result.firstId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
PnpOperation.SetAci(
|
||||
recipientId = result.firstId,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164Only_pniAndE164_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, null, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(
|
||||
recipientId = result.firstId,
|
||||
pni = PNI_A
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_pniOnly_noAciProvided() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(null, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.firstId,
|
||||
secondaryId = result.secondId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null),
|
||||
Input(E164_B, PNI_A, null),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_B, null, pniSession = true),
|
||||
Input(E164_B, PNI_A, null, pniSession = true),
|
||||
),
|
||||
Update(E164_A, PNI_A, null),
|
||||
Output(E164_A, PNI_A, null)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.firstId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.SetPni(result.firstId, PNI_A),
|
||||
PnpOperation.SessionSwitchoverInsert(result.secondId),
|
||||
PnpOperation.SessionSwitchoverInsert(result.firstId)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_B, PNI_A, null),
|
||||
Input(null, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.firstId),
|
||||
PnpOperation.Update(
|
||||
recipientId = result.secondId,
|
||||
e164 = E164_A,
|
||||
pni = PNI_A,
|
||||
aci = ACI_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, PNI_B, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemovePni(result.secondId),
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_e164AndPni_e164Aci_changeNumber() {
|
||||
val result = applyAndAssert(
|
||||
listOf(
|
||||
Input(E164_A, PNI_A, null),
|
||||
Input(E164_B, null, ACI_A),
|
||||
),
|
||||
Update(E164_A, PNI_A, ACI_A),
|
||||
Output(E164_A, PNI_A, ACI_A)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
PnpChangeSet(
|
||||
id = PnpIdResolver.PnpNoopId(result.secondId),
|
||||
operations = listOf(
|
||||
PnpOperation.RemoveE164(result.secondId),
|
||||
PnpOperation.Merge(
|
||||
primaryId = result.secondId,
|
||||
secondaryId = result.firstId
|
||||
),
|
||||
PnpOperation.ChangeNumberInsert(
|
||||
recipientId = result.secondId,
|
||||
oldE164 = E164_B,
|
||||
newE164 = E164_A
|
||||
)
|
||||
)
|
||||
),
|
||||
result.changeSet
|
||||
)
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientDatabase.PNI_COLUMN to pni?.toString(),
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
|
||||
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
|
||||
SignalDatabase.rawDatabase.insert(
|
||||
SessionDatabase.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
SessionDatabase.ACCOUNT_ID to account.toString(),
|
||||
SessionDatabase.ADDRESS to address.toString(),
|
||||
SessionDatabase.DEVICE to 1,
|
||||
SessionDatabase.RECORD to Util.getSecretBytes(32)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
|
||||
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
|
||||
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
|
||||
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
|
||||
val id
|
||||
get() = if (ids.size == 1) {
|
||||
ids[0]
|
||||
} else {
|
||||
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
|
||||
}
|
||||
|
||||
val firstId
|
||||
get() = ids[0]
|
||||
|
||||
val secondId
|
||||
get() = ids[1]
|
||||
|
||||
val thirdId
|
||||
get() = ids[2]
|
||||
}
|
||||
|
||||
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
|
||||
return applyAndAssert(listOf(input), update, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
|
||||
* and then verify your output matches what you expect.
|
||||
*
|
||||
* It results the inserted ID's and changeset for additional verification.
|
||||
*
|
||||
* But basically this is here to make the tests more readable. It gives you a clear list of:
|
||||
* - input
|
||||
* - update
|
||||
* - output
|
||||
*
|
||||
* that you can spot check easily.
|
||||
*
|
||||
* Important: The output will only include records that contain fields from the input. That means
|
||||
* for:
|
||||
*
|
||||
* Input: E164_B, PNI_A, null
|
||||
* Update: E164_A, PNI_A, null
|
||||
*
|
||||
* You will get:
|
||||
* Output: E164_A, PNI_A, null
|
||||
*
|
||||
* Even though there was an update that will also result in the row (E164_B, null, null)
|
||||
*/
|
||||
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
|
||||
val ids = input.map { insert(it.e164, it.pni, it.aci) }
|
||||
|
||||
input
|
||||
.filter { it.pniSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
|
||||
|
||||
input
|
||||
.filter { it.aciSession }
|
||||
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
|
||||
|
||||
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
|
||||
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
|
||||
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
|
||||
|
||||
val data = PnpDataSet(
|
||||
e164 = update.e164,
|
||||
pni = update.pni,
|
||||
aci = update.aci,
|
||||
byE164 = byE164,
|
||||
byPniSid = byPniSid,
|
||||
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
|
||||
byAciSid = byAciSid,
|
||||
e164Record = byE164?.let { db.getRecord(it) },
|
||||
pniSidRecord = byPniSid?.let { db.getRecord(it) },
|
||||
aciSidRecord = byAciSid?.let { db.getRecord(it) }
|
||||
)
|
||||
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
|
||||
|
||||
val finalData = data.perform(changeSet.operations)
|
||||
|
||||
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
|
||||
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
|
||||
|
||||
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
|
||||
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
|
||||
|
||||
return PnpMatchResult(
|
||||
ids = ids,
|
||||
changeSet = changeSet
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
|
||||
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
|
||||
|
||||
const val E164_A = "+12221234567"
|
||||
const val E164_B = "+13331234567"
|
||||
}
|
||||
}
|
|
@ -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].
|
||||
|
|
|
@ -16,6 +16,14 @@ public final class ACI extends ServiceId {
|
|||
return new ACI(uuid);
|
||||
}
|
||||
|
||||
public static ACI from(ServiceId serviceId) {
|
||||
return new ACI(serviceId.uuid());
|
||||
}
|
||||
|
||||
public static ACI fromNullable(ServiceId serviceId) {
|
||||
return serviceId != null ? new ACI(serviceId.uuid()) : null;
|
||||
}
|
||||
|
||||
public static ACI parseOrThrow(String raw) {
|
||||
return from(UUID.fromString(raw));
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ public final class Preconditions {
|
|||
}
|
||||
}
|
||||
|
||||
public static void checkState(boolean state) {
|
||||
checkState(state, "Condition must be true!");
|
||||
}
|
||||
|
||||
public static void checkState(boolean state, String message) {
|
||||
if (!state) {
|
||||
throw new IllegalStateException(message);
|
||||
|
|
Loading…
Add table
Reference in a new issue