diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt index d16c828bca..b50c8169a9 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt @@ -8,6 +8,9 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.signal.core.util.ThreadUtil +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet import org.thoughtcrime.securesms.keyvalue.KeyValueStore @@ -262,8 +265,11 @@ class RecipientDatabaseTest { /** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */ @Test fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() { - val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A) - val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A) + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true) val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) assertEquals(existingAciId, retrievedId) @@ -274,6 +280,32 @@ class RecipientDatabaseTest { val existingE164Recipient = Recipient.resolved(existingE164Id) assertEquals(retrievedId, existingE164Recipient.id) + + changeNumberListener.waitForJobManager() + assertFalse(changeNumberListener.numberChangeWasEnqueued) + } + + /** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust], but with a number change. */ + @Test + fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true) + val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) + assertEquals(existingAciId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireAci()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + + val existingE164Recipient = Recipient.resolved(existingE164Id) + assertEquals(retrievedId, existingE164Recipient.id) + + changeNumberListener.waitForJobManager() + assert(changeNumberListener.numberChangeWasEnqueued) } /** Low trust means you can’t merge. If you’re retrieving a user from the table with this data, prefer the ACI one. */ @@ -297,6 +329,9 @@ class RecipientDatabaseTest { /** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */ @Test fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true) val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true) @@ -310,6 +345,8 @@ class RecipientDatabaseTest { val existingRecipient2 = Recipient.resolved(existingId2) assertEquals(ACI_B, existingRecipient2.requireAci()) assertFalse(existingRecipient2.hasE164()) + + assert(changeNumberListener.numberChangeWasEnqueued) } /** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */ @@ -376,6 +413,63 @@ class RecipientDatabaseTest { assertEquals(E164_A, recipientWithId1.requireE164()) } + /** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfFalse() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_A.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = false) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireAci()) + assertEquals(E164_A, retrievedRecipient.requireE164()) + } + + /** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfTrue() { + val dataSet = KeyValueDataSet().apply { + putString(AccountValues.KEY_E164, E164_A) + putString(AccountValues.KEY_ACI, ACI_A.toString()) + } + SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = true) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireAci()) + assertEquals(E164_B, retrievedRecipient.requireE164()) + } + + /** Verifying a case where a change number job is expected to be enqueued. */ + @Test + fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_highTrust_changedNumber() { + val changeNumberListener = ChangeNumberListener() + changeNumberListener.enqueue() + + val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) + + val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true) + assertEquals(existingId, retrievedId) + + val retrievedRecipient = Recipient.resolved(retrievedId) + assertEquals(ACI_A, retrievedRecipient.requireAci()) + assertEquals(E164_B, retrievedRecipient.requireE164()) + + changeNumberListener.waitForJobManager() + assert(changeNumberListener.numberChangeWasEnqueued) + } + // ============================================================== // Misc // ============================================================== @@ -426,6 +520,24 @@ class RecipientDatabaseTest { } } + private class ChangeNumberListener { + + var numberChangeWasEnqueued = false + private set + + fun waitForJobManager() { + ApplicationDependencies.getJobManager().flush() + ThreadUtil.sleep(500) + } + + fun enqueue() { + ApplicationDependencies.getJobManager().addListener( + { job -> job.factoryKey == RecipientChangedNumberJob.KEY }, + { _, _ -> numberChangeWasEnqueued = true } + ) + } + } + 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")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 64625913c5..6d79418be6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -398,152 +398,76 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun getAndPossiblyMerge(aci: ACI?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientId { require(!(aci == null && e164 == null)) { "Must provide an ACI or E164!" } - var recipientNeedingRefresh: RecipientId? = null - var remapped: Pair? = null - var recipientChangedNumber: RecipientId? = null - var transactionSuccessful = false val db = writableDatabase + var transactionSuccessful = false + var remapped: Pair? = null + var recipientsNeedingRefresh: List = listOf() + var recipientChangedNumber: RecipientId? = null + db.beginTransaction() try { - val byE164 = e164?.let { getByE164(it) } ?: Optional.absent() - val byAci = aci?.let { getByAci(it) } ?: Optional.absent() - val finalId: RecipientId + val fetch: RecipientFetch = fetchRecipient(aci, e164, highTrust, changeSelf) - if (!byE164.isPresent && !byAci.isPresent) { - Log.i(TAG, "Discovered a completely new user. Inserting.", true) - finalId = if (highTrust) { - val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(e164, aci)) - RecipientId.from(id) - } else { - val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(if (aci == null) e164 else null, aci)) + if (fetch.logBundle != null) { + Log.w(TAG, fetch.toString()) + } + + val resolvedId: RecipientId = when (fetch) { + is RecipientFetch.Match -> { + fetch.id + } + is RecipientFetch.MatchAndUpdateE164 -> { + setPhoneNumber(fetch.id, fetch.e164) + recipientsNeedingRefresh = listOf(fetch.id) + recipientChangedNumber = fetch.changedNumber + fetch.id + } + is RecipientFetch.MatchAndReassignE164 -> { + removePhoneNumber(fetch.e164Id, db) + setPhoneNumber(fetch.id, fetch.e164) + recipientsNeedingRefresh = listOf(fetch.id, fetch.e164Id) + recipientChangedNumber = fetch.changedNumber + fetch.id + } + is RecipientFetch.MatchAndUpdateAci -> { + markRegistered(fetch.id, fetch.aci) + recipientsNeedingRefresh = listOf(fetch.id) + fetch.id + } + is RecipientFetch.MatchAndInsertAci -> { + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, fetch.aci)) RecipientId.from(id) } - } else if (byE164.isPresent && !byAci.isPresent) { - if (aci != null) { - val e164Record: RecipientRecord = getRecord(byE164.get()) - if (e164Record.aci != null) { - if (highTrust && e164Record.aci != SignalStore.account().aci) { - Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. High-trust, so stripping the E164 ($e164) from the existing account and assigning it to a new entry.", true) - removePhoneNumber(byE164.get(), db) - recipientNeedingRefresh = byE164.get() - - val insertValues = buildContentValuesForNewUser(e164, aci) - insertValues.put(BLOCKED, if (e164Record.isBlocked) 1 else 0) - - val id = db.insert(TABLE_NAME, null, insertValues) - finalId = RecipientId.from(id) - } else { - if (e164Record.aci == SignalStore.account().aci) { - Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user is us! Likely a case where we changed our number, but the old owner is sending sealed sender messages. Keeping the E164 ($e164) for ourselves and making a new user for the ACI.", true) - } else { - Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. Low-trust, so making a new user for the ACI.", true) - } - val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)) - finalId = RecipientId.from(id) - } - } else { - finalId = if (highTrust) { - Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). High-trust, so updating.", true) - markRegisteredOrThrow(byE164.get(), aci) - byE164.get() - } else { - Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). Low-trust, so making a new user for the ACI.", true) - val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci)) - RecipientId.from(id) - } - } - } else { - finalId = byE164.get() + is RecipientFetch.MatchAndMerge -> { + remapped = Pair(fetch.e164Id, fetch.aciId) + val mergedId: RecipientId = merge(fetch.aciId, fetch.e164Id) + recipientsNeedingRefresh = listOf(mergedId) + recipientChangedNumber = fetch.changedNumber + mergedId } - } else if (!byE164.isPresent && byAci.isPresent) { - if (e164 != null) { - if (highTrust) { - if (aci == SignalStore.account().aci && !changeSelf) { - Log.w(TAG, "Found out about an E164 ($e164) for our own ACI user (${byAci.get()}). High-trust but not change self, doing nothing.", true) - finalId = byAci.get() - } else { - Log.i(TAG, "Found out about an E164 ($e164) for a known ACI user (${byAci.get()}). High-trust, so updating.", true) - val aciRecord: RecipientRecord = getRecord(byAci.get()) - setPhoneNumberOrThrow(byAci.get(), e164) - finalId = byAci.get() - - if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) { - recipientChangedNumber = finalId - } - } - } else { - Log.i(TAG, "Found out about an E164 ($e164) for a known ACI user (${byAci.get()}). Low-trust, so doing nothing.", true) - finalId = byAci.get() - } - } else { - finalId = byAci.get() + is RecipientFetch.Insert -> { + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(fetch.e164, fetch.aci)) + RecipientId.from(id) } - } else { - if (byE164 == byAci) { - finalId = byAci.get() - } else { - Log.w(TAG, "Hit a conflict between ${byE164.get()} (E164 of $e164) and ${byAci.get()} (ACI $aci). They map to different recipients.", Throwable(), true) - val e164Record: RecipientRecord = getRecord(byE164.get()) - if (e164Record.aci != null) { - if (highTrust && e164Record.aci != SignalStore.account().aci) { - Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci). Likely a case of re-registration. High-trust, so stripping the E164 ($e164) from the existing account and assigning it to the ACI entry.", true) - removePhoneNumber(byE164.get(), db) - - recipientNeedingRefresh = byE164.get() - val aciRecord: RecipientRecord = getRecord(byAci.get()) - setPhoneNumberOrThrow(byAci.get(), e164!!) - finalId = byAci.get() - - if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) { - recipientChangedNumber = finalId - } - } else { - if (e164Record.aci == SignalStore.account().aci) { - Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci), but the E164 contact is us! Likely a case where we changed our number, but the old owner is sending sealed sender messages. Keeping the E164 ($e164) for ourselves.", true) - } else { - Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci). Likely a case of re-registration. Low-trust, so doing nothing.", true) - } - finalId = byAci.get() - } - } else { - val aciRecord: RecipientRecord = getRecord(byAci.get()) - if (aciRecord.e164 != null) { - if (highTrust) { - Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. High-trust, so merging the two rows together. The E164 has also effectively changed for the ACI contact.", true) - finalId = merge(byAci.get(), byE164.get()) - recipientNeedingRefresh = byAci.get() - remapped = Pair(byE164.get(), byAci.get()) - recipientChangedNumber = finalId - } else { - Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. Low-trust, so doing nothing.", true) - finalId = byAci.get() - } - } else { - if (highTrust) { - Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. High-trust, so merging the two rows together.", true) - finalId = merge(byAci.get(), byE164.get()) - recipientNeedingRefresh = byAci.get() - remapped = Pair(byE164.get(), byAci.get()) - } else { - Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. Low-trust, so doing nothing.", true) - finalId = byAci.get() - } - } - } + is RecipientFetch.InsertAndReassignE164 -> { + removePhoneNumber(fetch.e164Id, db) + recipientsNeedingRefresh = listOf(fetch.e164Id) + val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(fetch.e164, fetch.aci)) + RecipientId.from(id) } } - db.setTransactionSuccessful() transactionSuccessful = true - return finalId + db.setTransactionSuccessful() + return resolvedId } finally { db.endTransaction() if (transactionSuccessful) { - if (recipientNeedingRefresh != null) { - Recipient.live(recipientNeedingRefresh).refresh() - RetrieveProfileJob.enqueue(recipientNeedingRefresh) + if (recipientsNeedingRefresh.isNotEmpty()) { + recipientsNeedingRefresh.forEach { Recipient.live(it).refresh() } + RetrieveProfileJob.enqueue(recipientsNeedingRefresh.toSet()) } if (remapped != null) { @@ -551,7 +475,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : ApplicationDependencies.getRecipientCache().remap(remapped.first(), remapped.second()) } - if (recipientNeedingRefresh != null || remapped != null) { + if (recipientsNeedingRefresh.isNotEmpty() || remapped != null) { StorageSyncHelper.scheduleSyncForDataChange() RecipientId.clearCache() } @@ -563,6 +487,83 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + private fun fetchRecipient(aci: ACI?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientFetch { + val byE164 = e164?.let { getByE164(it) } ?: Optional.absent() + val byAci = aci?.let { getByAci(it) } ?: Optional.absent() + + var logs = LogBundle( + byAci = byAci.transform { id -> RecipientLogDetails(id = id) }.orNull(), + byE164 = byE164.transform { id -> RecipientLogDetails(id = id) }.orNull(), + label = "L0" + ) + + if (byAci.isPresent && byE164.isPresent && byAci.get() == byE164.get()) { + return RecipientFetch.Match(byAci.get(), null) + } + + if (byAci.isPresent && byE164.isAbsent()) { + val aciRecord: RecipientRecord = getRecord(byAci.get()) + logs = logs.copy(byAci = aciRecord.toLogDetails()) + + if (highTrust && e164 != null && (changeSelf || aci != SignalStore.account().aci)) { + val changedNumber: RecipientId? = if (aciRecord.e164 != null && aciRecord.e164 != e164) aciRecord.id else null + return RecipientFetch.MatchAndUpdateE164(byAci.get(), e164, changedNumber, logs.label("L1")) + } else if (e164 == null) { + return RecipientFetch.Match(byAci.get(), null) + } else { + return RecipientFetch.Match(byAci.get(), logs.label("L2")) + } + } + + if (byAci.isAbsent() && byE164.isPresent) { + val e164Record: RecipientRecord = getRecord(byE164.get()) + logs = logs.copy(byE164 = e164Record.toLogDetails()) + + if (highTrust && aci != null && e164Record.aci == null) { + return RecipientFetch.MatchAndUpdateAci(byE164.get(), aci, logs.label("L3")) + } else if (highTrust && aci != null && e164Record.aci != SignalStore.account().aci) { + return RecipientFetch.InsertAndReassignE164(aci, e164, byE164.get(), logs.label("L4")) + } else if (aci != null) { + return RecipientFetch.Insert(aci, null, logs.label("L5")) + } else { + return RecipientFetch.Match(byE164.get(), null) + } + } + + if (byAci.isAbsent() && byE164.isAbsent()) { + if (highTrust) { + return RecipientFetch.Insert(aci, e164, logs.label("L6")) + } else if (aci != null) { + return RecipientFetch.Insert(aci, null, logs.label("L7")) + } else { + return RecipientFetch.Insert(null, e164, logs.label("L8")) + } + } + + require(byAci.isPresent && byE164.isPresent && byAci.get() != byE164.get()) { "Assumed conditions at this point." } + + val aciRecord: RecipientRecord = getRecord(byAci.get()) + val e164Record: RecipientRecord = getRecord(byE164.get()) + + logs = logs.copy(byAci = aciRecord.toLogDetails(), byE164 = e164Record.toLogDetails()) + + if (e164Record.aci == null) { + if (highTrust) { + val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null + return RecipientFetch.MatchAndMerge(aciId = byAci.get(), e164Id = byE164.get(), changedNumber = changedNumber, logs.label("L9")) + } else { + return RecipientFetch.Match(byAci.get(), logs.label("L10")) + } + } else { + if (highTrust && e164Record.aci != SignalStore.account().aci) { + val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null + return RecipientFetch.MatchAndReassignE164(id = byAci.get(), e164Id = byE164.get(), e164 = e164!!, changedNumber = changedNumber, logs.label("L11")) + } else { + return RecipientFetch.Match(byAci.get(), logs.label("L12")) + } + } + } + fun getOrInsertFromAci(aci: ACI): RecipientId { return getOrInsertByColumn(ACI_COLUMN, aci.toString()).recipientId } @@ -2985,6 +2986,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column" } + private fun Optional.isAbsent(): Boolean { + return !this.isPresent + } + + private fun RecipientRecord.toLogDetails(): RecipientLogDetails { + return RecipientLogDetails( + id = this.id, + aci = this.aci, + e164 = this.e164 + ) + } + inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) { private val pendingRecipients: MutableSet = mutableSetOf() @@ -3272,4 +3285,70 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } } + + private sealed class RecipientFetch(val logBundle: LogBundle?) { + /** + * We have a matching recipient, and no writes need to occur. + */ + data class Match(val id: RecipientId, val bundle: LogBundle?) : RecipientFetch(bundle) + + /** + * We found a matching recipient and can update them with a new E164. + */ + data class MatchAndUpdateE164(val id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * We found a matching recipient and can give them an E164 that used to belong to someone else. + */ + data class MatchAndReassignE164(val id: RecipientId, val e164Id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * We found a matching recipient and can update them with a new ACI. + */ + data class MatchAndUpdateAci(val id: RecipientId, val aci: ACI, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * We found a matching recipient and can insert an ACI as a *new user*. + */ + data class MatchAndInsertAci(val id: RecipientId, val aci: ACI, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * The ACI maps to ACI-only recipient, and the E164 maps to a different E164-only recipient. We need to merge the two together. + */ + data class MatchAndMerge(val aciId: RecipientId, val e164Id: RecipientId, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * We don't have a matching recipient, so we need to insert one. + */ + data class Insert(val aci: ACI?, val e164: String?, val bundle: LogBundle) : RecipientFetch(bundle) + + /** + * We need to create a new recipient and give it the E164 of an existing recipient. + */ + data class InsertAndReassignE164(val aci: ACI?, val e164: String?, val e164Id: RecipientId, val bundle: LogBundle) : RecipientFetch(bundle) + } + + /** + * Simple class for [fetchRecipient] to pass back info that can be logged. + */ + private data class LogBundle( + val label: String, + val aci: ACI? = null, + val e164: String? = null, + val byAci: RecipientLogDetails? = null, + val byE164: RecipientLogDetails? = null + ) { + fun label(label: String): LogBundle { + return this.copy(label = label) + } + } + + /** + * Minimal info about a recipient that we'd want to log. Used in [fetchRecipient]. + */ + private data class RecipientLogDetails( + val id: RecipientId, + val aci: ACI? = null, + val e164: String? = null + ) }